npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

tp-link-legacy-api

v0.4.0

Published

Client Node.js pour l'interface web des routeurs TP-Link « legacy » (TL-WR841N v13/v14 et proches, firmwares /cgi_gdpr)

Readme

tp-link-legacy-api

"Buy Me A Coffee" "Buy Me A Coffee"

Read this in French.

Node.js client (ESM, zero dependency) for the web interface of legacy TP-Link routers — the firmwares exposing /cgi_gdpr, including the TL-WR841N v13/v14.

It speaks the router's protocol directly: no browser, no execution of the firmware's own scripts.

node deps license

Install

npm install tp-link-legacy-api

Node ≥ 18.

Library

import { TpLinkRouter } from "tp-link-legacy-api";

const router = new TpLinkRouter({ host: "192.168.0.1", password: "…" });

await router.getInfo();      // model, firmware, uptime, mode
await router.getLan();       // IP, netmask, MAC, DHCP state
await router.getWan();       // internet connection, public IP, DNS, link
await router.getWireless();  // radios: SSID, channel, security, enabled
await router.getClients();   // DHCP leases + Wi-Fi stations, deduplicated
await router.getStatus();    // everything at once

await router.setWirelessEnabled(false, { band: "2.4GHz" });
await router.setSsid("MyNetwork", { band: "2.4GHz" });
await router.reboot();

await router.disconnect();   // frees the administrator slot

getStatus() isolates each section: a section the firmware refuses returns its error under status.errors without failing the rest.

For anything the high-level API does not cover, the data model stays reachable:

await router.raw.getList("LAN_WLAN_ASSOC_DEV");
await router.raw.get("IGD_DEV_INFO", { attrs: ["upTime"] });
await router.raw.set("LAN_WLAN", { channel: 6 }, { stack: "1,1,0,0,0,0" });

Command line

tp-link-api status  --host 192.168.0.1 --password '…'
tp-link-api clients --host 192.168.0.1 --password '…'
tp-link-api wifi    --host 192.168.0.1 --password '…' --off --band 2.4GHz
tp-link-api get LAN_WLAN --list --host 192.168.0.1 --password '…'

REST server

Meant as a source for Home Assistant.

tp-link-api serve --config routers.json --port 8787 --token MY_TOKEN
[
  { "name": "living-room", "host": "192.168.11.1", "password": "…" },
  { "name": "bedroom",     "host": "192.168.12.1", "password": "…" }
]

| Route | Effect | |---|---| | GET /health | service state | | GET /routers | router list (?status=1 to read them all) | | GET /routers/:name/status | full snapshot | | GET /routers/:name/{info,lan,wan,wireless,clients,leases,ports} | one section | | POST /routers/:name/wireless/:band/enable | { "enabled": false } | | POST /routers/:name/wireless/:band/ssid | { "ssid": "…" } | | POST /routers/:name/reboot | reboot |

Environment variables work too: TPLINK_ROUTERS, TPLINK_HOST, TPLINK_PASSWORD, TPLINK_PORT, TPLINK_TOKEN.

Wi-Fi passphrases are never returned by default, even though the firmware hands them over in clear; --secrets or ?secrets=1 is required.

⚠️ The client must be on the router's LAN

The firmware enforces a "GDPR" restriction: objects holding personal data — Wi-Fi passphrase, MAC addresses, WAN credentials — are only readable by a local client. From another subnet the router answers HTTP 500 on those objects and exposes only model, firmware and mode.

Check it with:

tp-link-api get /cgi/info --host 192.168.0.1 --password '…'
# clientLocal=1  → everything is readable
# clientLocal=0  → only non-personal data is

This comes from the router, not from this library: the firmware's own JavaScript receives the same HTTP 500 in that situation. Run the API from a machine on that LAN — the Home Assistant host, typically.

The protocol, briefly

Reverse-engineered from js/lib.js and js/tpEncrypt.js served by the router.

  1. POST /cgi?8 with [/cgi/getParm#…]0,0 → 512-bit RSA public key (nn, ee) and session counter (seq).
  2. The client draws an AES-128-CBC key and IV (16 digits each).
  3. Every request is POST /cgi_gdpr:
    sign=<RSA hex>\r\ndata=<AES base64>\r\n
    data encrypts <types>\r\n[<oid>#<stack>#<pStack>]<index>,<n>\r\n<attributes>, sign encrypts key=…&iv=…&h=md5(user+password)&s=<seq+len(data)>.
  4. The response is AES base64, decrypted with the same key.

Two traps, for anyone reimplementing this:

  • The Referer header is mandatory: without it the router answers 403 on every resource, including its own .js files.
  • The signature must replay key=…&iv=… on every request. The firmware also offers a short form (h=…&s=…) which assumes the router still holds the session key in memory; once that context is lost it produces a 500 and voids the cookie. The full form makes the session self-contained.

The embedded httpd also earns its own HTTP client (src/core/http.js): it announces Transfer-Encoding: chunked on error pages but sends the body raw, which breaks both fetch() and node:http, even with insecureHTTPParser.

Home Assistant

A Home Assistant integration built on a Python port of this client lives at GollumDom/tp-link-legacy-integration.

Tests

npm test

The tests cover the protocol (RSA, serialization, response parsing, encryption) and need no router.

License

MIT