@hanoilab/zk-bridge 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +180 -103
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,43 +1,119 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://img.shields.io/badge/ZK--Bridge-Attendance-007ACC?style=for-the-badge&logo=fingerprint&logoColor=white" alt="ZK-Bridge" />
|
|
3
|
+
</p>
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
<h1 align="center">ZK-Bridge</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>LAN-side bridge for ZKTeco attendance devices.</strong><br/>
|
|
9
|
+
Polls a ZKTeco fingerprint reader over TCP, pushes events to any HTTP backend.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<a href="https://www.npmjs.com/package/@hanoilab/zk-bridge">npm</a> •
|
|
14
|
+
<a href="#quick-start">Quick Start</a> •
|
|
15
|
+
<a href="#features">Features</a> •
|
|
16
|
+
<a href="#how-it-works">How It Works</a> •
|
|
17
|
+
<a href="#backend-contract">Backend Contract</a> •
|
|
18
|
+
<a href="#self-hosting">Self-Hosting</a>
|
|
19
|
+
</p>
|
|
20
|
+
|
|
21
|
+
<p align="center">
|
|
22
|
+
<a href="https://www.npmjs.com/package/@hanoilab/zk-bridge"><img src="https://img.shields.io/npm/v/@hanoilab/zk-bridge?style=flat-square&color=cb3837&label=npm" alt="npm" /></a>
|
|
23
|
+
<img src="https://img.shields.io/badge/node-%3E%3D20.0.0-339933?style=flat-square&logo=node.js&logoColor=white" alt="node" />
|
|
24
|
+
<img src="https://img.shields.io/badge/sqlite-bundled-003B57?style=flat-square&logo=sqlite&logoColor=white" alt="sqlite" />
|
|
25
|
+
<img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="license" />
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## What is this?
|
|
31
|
+
|
|
32
|
+
ZKTeco fingerprint readers don't speak HTTP — they only accept TCP connections from inside the LAN, and the C-HR / HRIS backend lives in the cloud. ZK-Bridge sits in the office, polls the device on a schedule, and pushes attendance events to any HTTP API you point it at.
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
ZKTeco device ZK-Bridge Your backend
|
|
36
|
+
┌─────────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
37
|
+
│ Fingerprint / │ ◄─TCP─┤ CLI + UI │ ◄──HTTPS──┤ /push │
|
|
38
|
+
│ face reader │ 4370 │ SQLite │ │ /ping │
|
|
39
|
+
│ 192.168.x.y │ │ Local LAN │ │ Cloud / VPS │
|
|
40
|
+
└─────────────────┘ └──────────────┘ └──────────────┘
|
|
41
|
+
Outbound only — no port
|
|
42
|
+
forwarding or VPN needed
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
No vendor lock-in: any backend that exposes a JSON `POST` endpoint with a JWT auth header works. Bridge handles the LAN side.
|
|
4
46
|
|
|
5
47
|
## Quick Start
|
|
6
48
|
|
|
49
|
+
### 1. Install globally
|
|
50
|
+
|
|
7
51
|
```bash
|
|
8
|
-
# Install globally
|
|
9
52
|
npm i -g @hanoilab/zk-bridge
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 2. Run it
|
|
10
56
|
|
|
11
|
-
|
|
57
|
+
```bash
|
|
12
58
|
zk-bridge start
|
|
13
59
|
```
|
|
14
60
|
|
|
15
|
-
That's it. You'll see:
|
|
16
|
-
|
|
17
61
|
```text
|
|
18
62
|
[zk-bridge] data dir: ~/.local/share/zk-bridge
|
|
19
63
|
[zk-bridge] UI listening on http://127.0.0.1:7000
|
|
20
64
|
[scheduler] starting cron "*/5 * * * *" (every 5 min)
|
|
65
|
+
[zk-bridge] running. Press Ctrl+C to stop.
|
|
21
66
|
```
|
|
22
67
|
|
|
23
|
-
Open the admin UI
|
|
68
|
+
### 3. Open the admin UI
|
|
24
69
|
|
|
25
|
-
|
|
70
|
+
Visit **<http://localhost:7000>**, set the backend Push URL, paste the per-device JWT — bridge pushes attendance on the next cycle.
|
|
26
71
|
|
|
27
|
-
|
|
28
|
-
- **LAN scan** — Auto-discover ZKTeco devices on your subnet (TCP 4370)
|
|
29
|
-
- **Multi-device** — One bridge polls many devices on a schedule
|
|
30
|
-
- **Offline tolerant** — Queues events when the backend is unreachable, drains on next cycle
|
|
31
|
-
- **Idempotent push** — Backend dedupes by `(deviceId, eventLogId)`; safe to retry
|
|
32
|
-
- **Auto-start on boot** — One-click systemd / Windows Task / launchd registration
|
|
33
|
-
- **Cross-platform** — Linux, macOS, Windows
|
|
34
|
-
- **Secure** — bcrypt admin login, signed-cookie sessions, JWT per-device tokens
|
|
72
|
+
## Features
|
|
35
73
|
|
|
36
|
-
|
|
74
|
+
### Web Admin UI
|
|
75
|
+
- Single-user login (bcrypt + signed-cookie session, 7-day TTL)
|
|
76
|
+
- Configure backend Push / Ping URL + poll interval
|
|
77
|
+
- Add devices manually or via LAN scan
|
|
78
|
+
- Per-device events with cursor position + push status badge
|
|
79
|
+
- Cycle history with status, timing, error message, filter by device
|
|
80
|
+
|
|
81
|
+
### LAN Discovery
|
|
82
|
+
- Auto-scan `/24` subnet for ZKTeco devices on TCP 4370
|
|
83
|
+
- Identify each candidate over the ZK protocol (model, serial)
|
|
84
|
+
- One-click "Add as device" pre-fills name + host
|
|
85
|
+
|
|
86
|
+
### Multi-Device
|
|
87
|
+
- One bridge polls many devices on a single schedule
|
|
88
|
+
- Per-device cursor, queue, audit log, error state
|
|
89
|
+
- Enable / disable individually without removing config
|
|
90
|
+
|
|
91
|
+
### Offline Tolerance
|
|
92
|
+
- Queues events to local SQLite when the backend is unreachable
|
|
93
|
+
- Drains the queue on the next online cycle
|
|
94
|
+
- Cursor advances even on partial failure — no data loss, no double-send
|
|
95
|
+
|
|
96
|
+
### Idempotent Push
|
|
97
|
+
- Backend dedupes by `(deviceId, eventLogId)` — replay is a no-op
|
|
98
|
+
- Batches events at 200/request to stay under common body-parser limits
|
|
99
|
+
- JWT version counter for revocation (regenerate on the backend → old JWTs rejected immediately)
|
|
100
|
+
|
|
101
|
+
### Auto-Start on Boot
|
|
102
|
+
- One-click toggle in the System page registers a:
|
|
103
|
+
- **systemd** unit (Linux)
|
|
104
|
+
- **Scheduled Task** (Windows)
|
|
105
|
+
- **launchd** plist (macOS)
|
|
106
|
+
- `Restart=on-failure` so a crashed cycle never takes the bridge down
|
|
107
|
+
|
|
108
|
+
### Cross-platform
|
|
109
|
+
- Linux, macOS, Windows
|
|
110
|
+
- Node 20+ — no native build needed (sqlite3 ships prebuilds)
|
|
111
|
+
|
|
112
|
+
## CLI
|
|
37
113
|
|
|
38
114
|
```bash
|
|
39
|
-
zk-bridge start # Start bridge (UI + scheduler)
|
|
40
|
-
zk-bridge poll-once #
|
|
115
|
+
zk-bridge start # Start bridge (UI + scheduler)
|
|
116
|
+
zk-bridge poll-once # Run a single cycle then exit
|
|
41
117
|
zk-bridge reset-user # Forgot-password recovery
|
|
42
118
|
zk-bridge recent-events # Print last N events from a device
|
|
43
119
|
zk-bridge upgrade [tag] # Self-update via npm
|
|
@@ -45,28 +121,15 @@ zk-bridge --help
|
|
|
45
121
|
zk-bridge --version
|
|
46
122
|
```
|
|
47
123
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
After `zk-bridge start`, open `http://localhost:7000` to:
|
|
124
|
+
Environment overrides (otherwise default):
|
|
51
125
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-
|
|
55
|
-
|
|
56
|
-
- Toggle auto-start on boot
|
|
126
|
+
```bash
|
|
127
|
+
PORT=8080 BIND_HOST=0.0.0.0 zk-bridge start
|
|
128
|
+
DATA_DIR=/var/lib/zk-bridge zk-bridge start
|
|
129
|
+
```
|
|
57
130
|
|
|
58
131
|
## How It Works
|
|
59
132
|
|
|
60
|
-
```text
|
|
61
|
-
ZK device zk-bridge Your backend
|
|
62
|
-
+----------+ +-----------+ +------------+
|
|
63
|
-
| ZKTeco | <-----> | bridge | <-----> | HTTP API |
|
|
64
|
-
| TCP 4370 | ZK | (this CLI)| HTTPS | /push |
|
|
65
|
-
+----------+ +-----------+ +------------+
|
|
66
|
-
SQLite
|
|
67
|
-
(config + queue)
|
|
68
|
-
```
|
|
69
|
-
|
|
70
133
|
Every cycle (default 5 min):
|
|
71
134
|
|
|
72
135
|
1. List enabled devices in local SQLite.
|
|
@@ -74,20 +137,32 @@ Every cycle (default 5 min):
|
|
|
74
137
|
3. Drain the offline queue, then push new events to the backend in batches of 200.
|
|
75
138
|
4. Advance the cursor, write a `cycle_log` row.
|
|
76
139
|
|
|
77
|
-
|
|
140
|
+
State that lives locally:
|
|
141
|
+
|
|
142
|
+
```
|
|
143
|
+
~/.local/share/zk-bridge/zk-bridge.db (Linux default)
|
|
144
|
+
%APPDATA%\zk-bridge\zk-bridge.db (Windows)
|
|
145
|
+
~/Library/Application Support/zk-bridge/zk-bridge.db (macOS)
|
|
146
|
+
|
|
147
|
+
├── users (single admin row)
|
|
148
|
+
├── config (push URL, ping URL, poll interval, session secret)
|
|
149
|
+
├── devices (host, port, JWT, cursor, last status)
|
|
150
|
+
├── event_queue (offline-pending events)
|
|
151
|
+
└── cycle_log (per-device cycle history, rotated to last 1000)
|
|
152
|
+
```
|
|
78
153
|
|
|
79
154
|
## Backend Contract
|
|
80
155
|
|
|
81
|
-
|
|
156
|
+
Two HTTP endpoints. Configure their full URLs in the UI — the bridge appends nothing.
|
|
82
157
|
|
|
83
|
-
|
|
158
|
+
### Push (required)
|
|
84
159
|
|
|
85
160
|
```http
|
|
86
161
|
POST <push-url>
|
|
87
162
|
Content-Type: application/json
|
|
88
163
|
|
|
89
164
|
{
|
|
90
|
-
"token": "<JWT>",
|
|
165
|
+
"token": "<JWT signed by backend>",
|
|
91
166
|
"events": [
|
|
92
167
|
{
|
|
93
168
|
"eventLogId": "12345",
|
|
@@ -99,7 +174,16 @@ Content-Type: application/json
|
|
|
99
174
|
}
|
|
100
175
|
```
|
|
101
176
|
|
|
102
|
-
|
|
177
|
+
The backend should:
|
|
178
|
+
|
|
179
|
+
- Verify the JWT (signature + expiry / version).
|
|
180
|
+
- Resolve the device row from the JWT payload.
|
|
181
|
+
- Dedupe by `(deviceId, eventLogId)` — replays are safe.
|
|
182
|
+
- Persist or normalize the events as needed.
|
|
183
|
+
|
|
184
|
+
Response shape isn't enforced — bridge only checks the HTTP status (2xx = success, anything else = retry / queue).
|
|
185
|
+
|
|
186
|
+
### Ping (optional)
|
|
103
187
|
|
|
104
188
|
```http
|
|
105
189
|
POST <ping-url>
|
|
@@ -108,100 +192,93 @@ Content-Type: application/json
|
|
|
108
192
|
{ "token": "<JWT>" }
|
|
109
193
|
```
|
|
110
194
|
|
|
111
|
-
If you don't expose a separate ping endpoint, leave the field
|
|
112
|
-
|
|
113
|
-
The backend is responsible for: verifying the JWT, resolving devices, deduping by `(deviceId, eventLogId)`, and storing or normalizing events.
|
|
114
|
-
|
|
115
|
-
## Requirements
|
|
116
|
-
|
|
117
|
-
- Node.js 20+
|
|
118
|
-
- macOS, Linux, or Windows
|
|
119
|
-
- Network reach: bridge host must see the ZK device on LAN AND the backend over HTTP(S)
|
|
195
|
+
Used by the **Connect** button to verify the JWT + URL without sending events. If you don't expose a separate ping endpoint, leave the field blank — the bridge falls back to a push with an empty `events` array (your backend should respond 4xx for that case, which the bridge interprets as "auth + URL OK").
|
|
120
196
|
|
|
121
|
-
|
|
197
|
+
### Reference implementation
|
|
122
198
|
|
|
123
|
-
|
|
199
|
+
See [c-hr backend](https://github.com/nguyendinhphongdx/c-hr) — `apps/backend/src/apps/attendance/attendance-device/` is a NestJS module that implements the contract end-to-end, including JWT version revocation and orphan-event reconcile.
|
|
124
200
|
|
|
125
|
-
|
|
126
|
-
| --- | --- | --- |
|
|
127
|
-
| `DATA_DIR` | OS-standard (see below) | Where SQLite + admin login live |
|
|
128
|
-
| `PORT` | `7000` | UI HTTP port |
|
|
129
|
-
| `BIND_HOST` | `127.0.0.1` | Listen address. Set `0.0.0.0` for LAN access |
|
|
201
|
+
## Self-Hosting
|
|
130
202
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
- Windows: `%APPDATA%\zk-bridge`
|
|
203
|
+
```bash
|
|
204
|
+
git clone https://github.com/nguyendinhphongdx/zkteco-bridge.git
|
|
205
|
+
cd zkteco-bridge
|
|
206
|
+
pnpm install
|
|
207
|
+
pnpm build
|
|
208
|
+
pnpm start
|
|
209
|
+
```
|
|
139
210
|
|
|
140
|
-
|
|
211
|
+
Or install your local checkout as the global CLI:
|
|
141
212
|
|
|
142
|
-
```
|
|
143
|
-
|
|
213
|
+
```bash
|
|
214
|
+
npm install -g .
|
|
215
|
+
zk-bridge start
|
|
144
216
|
```
|
|
145
217
|
|
|
146
|
-
|
|
218
|
+
### Auto-start (production)
|
|
147
219
|
|
|
148
|
-
Three options — pick one:
|
|
220
|
+
Three options — pick **one**, otherwise two processes will fight for port 7000:
|
|
149
221
|
|
|
150
|
-
- **Built-in toggle** (recommended)
|
|
151
|
-
- **PM2
|
|
222
|
+
- **Built-in toggle** (recommended) — *System → Auto-start on boot* registers a systemd / Windows Task / launchd entry pointing at the global CLI binary.
|
|
223
|
+
- **PM2** —
|
|
152
224
|
|
|
153
225
|
```bash
|
|
154
226
|
pm2 start "$(which zk-bridge)" --name zk-bridge -- start
|
|
155
227
|
pm2 startup && pm2 save
|
|
156
228
|
```
|
|
157
229
|
|
|
158
|
-
- **Docker** — see [`docker-compose.yml`](docker-compose.yml). Bind-mount `./data:/app/data` to persist
|
|
230
|
+
- **Docker** — see [`docker-compose.yml`](docker-compose.yml). Bind-mount `./data:/app/data` to persist SQLite across rebuilds.
|
|
159
231
|
|
|
160
|
-
|
|
232
|
+
## Environment Variables
|
|
161
233
|
|
|
162
|
-
|
|
234
|
+
| Variable | Default | Description |
|
|
235
|
+
|----------|---------|-------------|
|
|
236
|
+
| `PORT` | `7000` | Admin UI HTTP port |
|
|
237
|
+
| `BIND_HOST` | `127.0.0.1` | Listen address. Set `0.0.0.0` to allow LAN access |
|
|
238
|
+
| `DATA_DIR` | OS-standard user data dir | Override SQLite + admin login location |
|
|
239
|
+
| `PUSH_URL` | _(none)_ | First-run seed only — bridge stores it in SQLite then ignores env |
|
|
240
|
+
| `PING_URL` | _(none)_ | First-run seed only |
|
|
241
|
+
| `POLL_INTERVAL_MIN` | `5` | First-run seed only — minutes between cycles |
|
|
163
242
|
|
|
164
|
-
|
|
165
|
-
zk-bridge upgrade
|
|
166
|
-
sudo systemctl restart zk-bridge # or pm2 restart zk-bridge / docker compose pull
|
|
167
|
-
```
|
|
243
|
+
`DATA_DIR` resolution order on every start:
|
|
168
244
|
|
|
169
|
-
|
|
245
|
+
1. `DATA_DIR` env var (always wins)
|
|
246
|
+
2. `./data/` next to cwd, if it exists (Docker bind mount, dev workflow)
|
|
247
|
+
3. **Globally installed:** OS-standard user data dir
|
|
248
|
+
4. `./data/` next to cwd (dev fallback)
|
|
170
249
|
|
|
171
250
|
## Troubleshooting
|
|
172
251
|
|
|
173
252
|
| Symptom | Likely cause / fix |
|
|
174
|
-
|
|
175
|
-
| `ETIMEDOUT <ip>:<port>` on Connect | Bridge can't reach the backend Push URL
|
|
176
|
-
| `HTTP 401 Invalid token` | JWT was regenerated on the backend or device deleted. Re-paste the token. |
|
|
253
|
+
|---------|-------------------|
|
|
254
|
+
| `ETIMEDOUT <ip>:<port>` on Connect | Bridge can't reach the backend Push URL. `curl <url>` from the bridge host should work. |
|
|
255
|
+
| `HTTP 401 Invalid token` | JWT was regenerated on the backend, or the device row was deleted. Re-paste the token. |
|
|
177
256
|
| `Socket closed unexpectedly` | The ZK device only allows 1 active connection — another tool / cycle is holding it. Wait for the next cycle. |
|
|
178
|
-
| `port open but ZK probe fail` in scan | Same — device is busy. Try Add → Connect after a minute. |
|
|
257
|
+
| `port open but ZK probe fail` in scan | Same — device is busy. Try **Add** → **Connect** after a minute. |
|
|
179
258
|
| Dashboard shows "never run" | Push URL not set, or no devices configured. Check *API settings* + *Devices*. |
|
|
180
|
-
| Events arrive late | Lower the poll interval in *API settings* (min 1 min). |
|
|
259
|
+
| Events arrive late | Lower the *poll interval* in *API settings* (min 1 min). |
|
|
181
260
|
|
|
182
|
-
Every console line is prefixed with an ISO timestamp:
|
|
261
|
+
Every console line is prefixed with an ISO timestamp so logs from `pm2 logs` / `journalctl -u zk-bridge` / `docker compose logs` line up:
|
|
183
262
|
|
|
184
263
|
```text
|
|
185
264
|
[2026-05-07T08:23:50.747Z] [poll] "Front gate" pulled 2915 from ZK in 5291ms
|
|
186
265
|
```
|
|
187
266
|
|
|
188
|
-
##
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
git clone https://github.com/nguyendinhphongdx/c-hr.git
|
|
192
|
-
cd c-hr/services/zk-bridge
|
|
193
|
-
pnpm install
|
|
194
|
-
pnpm build
|
|
195
|
-
pnpm start
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
The package is standalone — it has its own `pnpm-workspace.yaml` and `pnpm-lock.yaml`, separate from any parent monorepo. Install your local copy as the global CLI:
|
|
267
|
+
## Tech Stack
|
|
199
268
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
269
|
+
| Layer | Technology |
|
|
270
|
+
|-------|-----------|
|
|
271
|
+
| Runtime | [Node.js 20+](https://nodejs.org/) |
|
|
272
|
+
| Local DB | [SQLite](https://www.sqlite.org/) via [sqlite3](https://github.com/TryGhost/node-sqlite3) + [Sequelize](https://sequelize.org/) |
|
|
273
|
+
| HTTP server | [Hono](https://hono.dev/) + [@hono/node-server](https://github.com/honojs/node-server) |
|
|
274
|
+
| ZK protocol | Custom client (`src/zklib`) — TCP raw, chunked streaming for large logs |
|
|
275
|
+
| Auth | [bcryptjs](https://github.com/dcodeIO/bcrypt.js) (admin) + JWT (per-device) |
|
|
276
|
+
| HTTP client | [axios](https://axios-http.com/) |
|
|
277
|
+
| Scheduler | [node-cron](https://github.com/node-cron/node-cron) |
|
|
278
|
+
| Build | [TypeScript](https://www.typescriptlang.org/) |
|
|
204
279
|
|
|
205
280
|
## License
|
|
206
281
|
|
|
207
|
-
MIT
|
|
282
|
+
MIT © [HanoiLab](mailto:opencode@hanoilab.vn)
|
|
283
|
+
|
|
284
|
+
---
|