zklib-ng
v0.1.8
Published
Pure-TypeScript ZKTeco standalone-device client (TCP/UDP 4370) — users, attendance, templates, realtime, access control. No native DLL.
Maintainers
Readme
zklib-ng
Pure-TypeScript client for ZKTeco standalone biometric / time-attendance /
access-control devices over TCP and UDP (port 4370). No native DLL, no
zkemkeeper COM dependency — the wire protocol is reimplemented in TypeScript on
a small, data-driven engine.
- ✅ Works over TCP or UDP, any platform (Linux/macOS/Windows/ARM/Docker)
- ✅ Users, attendance, fingerprint templates (read and upload), device info
- ✅ Online fingerprint enrollment, access control, holidays, bells, work codes
- ✅ Generic device-data table access — read/write/delete ~29 tables
- ✅ Auto-reconnect + retry, event-style real-time (
watch()), concurrency-safe - ✅ Cross-platform LAN discovery
- ✅ Clean namespaced API, dual ESM + CJS, zero runtime deps, Node ≥ 18
A ground-up v2 of
ZKLib-TSbuilt on a primitive + registry architecture: adding a command is a data entry, not a new codec. Device-verified against firmwareVer 6.60(platformZLM60_TFT).
Install
npm install zklib-ngQuick start
import { ZKDevice } from 'zklib-ng'
const zk = new ZKDevice({ host: '192.168.1.201' }) // port 4370, transport 'tcp'
await zk.connect()
console.log(await zk.device.firmware()) // "Ver 6.60 Oct 12 2021"
console.log(await zk.device.sizes()) // { users, fingers, records, ... }
const users = await zk.users.list() // User[]
const logs = await zk.attendance.list() // AttLog[] (Date timestamps)
await zk.disconnect()Options
new ZKDevice({
host: '192.168.1.201',
port: 4370, // default
password: 0, // comm key (default 0)
timeoutMs: 10000, // default
transport: 'tcp', // 'tcp' | 'udp'
autoReconnect: false, // rebuild + retry an op on network error
reconnectRetries: 1, // attempts when autoReconnect is on
})Namespaced API
All methods are async. Identity uses string userId (the PIN); uid is the
internal device slot.
zk.device.firmware() / serial() / fingerprintVersion() / time() / setTime(d) / sizes()
zk.options.get(key) / set(key, value) // device config (key=value)
zk.users.list() / get(userId) / set(user, packetSize?) / delete(uid)
zk.attendance.list() / clear()
zk.templates.list() / get(uid, fid) / set(user, fingers) / delete(uid, fid)
zk.tables.schemas() / read(name) / write(name, row) / delete(name, field, value)
zk.access.userGroup.get/set · timeZone.get/set · groupTimeZones.get/set · unlockGroup.get/set · holiday.get/set
zk.bell.set(schedule) / list()
zk.enroll.fingerprint({ userId, fingerId?, onProgress? }) / cancel() / verify() / regEvent(flags)
zk.oplog.list()
zk.photos.get(userId) / cards() // id_card table (binary-safe photo blob)
zk.faces.list() / get(userId) / delete(userId)
zk.roles.list() · zk.wiegand.list()
zk.liveCapture(signal?) // async generator of punch events
zk.liveEvents(opts?) // async generator — multiplexed events (attendance/finger/raw)
zk.watch(opts?) // EventEmitter (see below)
ZKDevice.discover(opts?) // static — find devices on the LANSee docs/sdk-coverage.md for the full SDK capability map.
Real-time events
const w = zk.watch({ autoReconnect: true })
w.on('attendance', ev => console.log(ev.userId, ev.timestamp, ev.status, ev.punch))
w.on('reconnecting', ({ attempt, error }) => console.warn('drop, retry', attempt, error.message))
w.on('reconnect', () => console.log('reconnected'))
w.on('error', err => console.error('gave up', err)) // only after retries exhausted
w.on('end', () => console.log('stopped'))
// ... later
w.stop() // ends the watcher even with autoReconnect onEach ZKDevice is independent (own socket, session, command queue), so one Node
process can watch a whole fleet concurrently. Operations on a single instance
are serialized internally — concurrent calls never corrupt the wire.
Discover devices on the LAN
const devices = await ZKDevice.discover() // [{ address, port }]
const detailed = await ZKDevice.discover({ enrich: true }) // + serialNumber, firmwareFleet — manage many devices as one
ZKFleet absorbs the multi-device plumbing so the app stays thin: one merged
realtime stream, broadcast user/template ops, error-isolated per device.
import { ZKFleet } from 'zklib-ng'
const fleet = new ZKFleet(['192.168.1.201', '192.168.1.202'], { autoReconnect: true })
await fleet.connectAll()
// one merged stream, each punch tagged with its device
fleet.watchAll().on('punch', ({ host, log }) => save(host, log))
// broadcast (error-isolated; returns a per-host { host, ok, error? }[])
await fleet.setUserEverywhere(user)
await fleet.distributeTemplate(user, fingers) // enroll once, push to all
await fleet.setTimeEverywhere(new Date())
// or build a fleet from discovery
const auto = await ZKFleet.discover()Gapless attendance feed
With autoReconnect, watch() (and fleet.watchAll()) is gapless: on every
reconnect it auto-pulls the log and emits the punches missed during the outage —
newer than the last emitted, de-duplicated by attLogKey. The app writes zero
catch-up logic; it just consumes attendance / punch events and stores them
under a unique key:
import { attLogKey } from 'zklib-ng'
// e.g. DB unique key per punch → idempotent inserts (the device re-delivers logs)
db.upsert({ id: attLogKey(log), ...log })Business logic (in/out evaluation, shift rules, scheduling) stays in the app.
Distribute a fingerprint across a fleet
Enroll once, then push the user + templates to every other device (templates are
portable between devices reporting the same device.fingerprintVersion()):
await source.users.set(user)
await source.enroll.fingerprint({ userId: user.userId, fingerId: 0 }) // press 3×
const fingers = (await source.templates.list()).filter(f => f.uid === user.uid)
for (const zk of others) {
await zk.users.set(user)
await zk.templates.set(user, fingers)
}Device support
Speaks the standalone ZK protocol on port 4370. Core (connect/auth, users, attendance, device info, templates, real-time) works broadly across BW/TFT/SSR firmware. Advanced features (device-data tables, access-control layouts, bell, enroll, template upload) were reverse-engineered and verified on firmware 6.60; other models may differ. Encrypted-channel firmware (newest models) is not supported.
ZK_HOST=192.168.1.201 npx ts-node examples/diagnose.ts
ZK_HOST=192.168.1.201 ZK_TRANSPORT=udp npx ts-node examples/diagnose.tsDevelopment
npm install
npm test # unit tests (no device needed; mock TCP/UDP servers)
npm run build # tsc -> dist/{cjs,esm,types}
npm run lint
npm run docs # typedoc -> docs/api/ (markdown API reference)Documentation
docs/api/— generated API reference (typedoc).docs/sdk-coverage.md— SDK capability coverage.docs/zkemkeeper-api-map.md— every zkemkeeper method (302) mapped to its zklib-ng equivalent.docs/app-attendance-strategy.md— how the official app manages users + attendance (reverse-engineered).
License
MIT.
Not affiliated with or endorsed by ZKTeco. "ZKTeco" is a trademark of its owner.
