@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.
Files changed (2) hide show
  1. package/README.md +180 -103
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,43 +1,119 @@
1
- # @hanoilab/zk-bridge
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
- LAN-side bridge for ZKTeco attendance devices. Polls a device over TCP and pushes events to your backend over HTTPS.
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> &bull;
14
+ <a href="#quick-start">Quick Start</a> &bull;
15
+ <a href="#features">Features</a> &bull;
16
+ <a href="#how-it-works">How It Works</a> &bull;
17
+ <a href="#backend-contract">Backend Contract</a> &bull;
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
- # Start the bridge
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, paste your backend's Push URL + a per-device JWT, and the bridge starts pushing on the next cycle.
68
+ ### 3. Open the admin UI
24
69
 
25
- ## Features
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
- - **Web admin UI** — Single-user login, device list, scan LAN, view events
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
- ## CLI Commands
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) — default
40
- zk-bridge poll-once # One cycle then exit
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
- ## Admin Panel
49
-
50
- After `zk-bridge start`, open `http://localhost:7000` to:
124
+ Environment overrides (otherwise default):
51
125
 
52
- - Set the backend Push URL + Ping URL + poll interval
53
- - Add devices (manual or via LAN scan)
54
- - View per-device events with cursor position, push status
55
- - Inspect cycle history, reset cursor, regenerate tokens
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
- Backend dedupes by `(deviceId, eventLogId)` — replays are safe.
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
- The bridge POSTs to **two URLs** you configure:
156
+ Two HTTP endpoints. Configure their full URLs in the UI — the bridge appends nothing.
82
157
 
83
- **Push URL** (required)
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
- **Ping URL** (optional — used by the *Connect* button)
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 empty — the bridge falls back to a push with an empty events array.
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
- ## Configuration
197
+ ### Reference implementation
122
198
 
123
- Settings live in a local SQLite DB and are edited through the web UI. Only paths and bind come from env:
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
- | Env | Default | Purpose |
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
- **Data dir resolution:**
132
-
133
- - `DATA_DIR` env (always wins)
134
- - `./data/` next to cwd, if it exists (Docker bind mount, dev workflow)
135
- - Globally installed: OS-standard user data dir
136
- - Linux: `~/.local/share/zk-bridge`
137
- - macOS: `~/Library/Application Support/zk-bridge`
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
- Boot prints the chosen path:
211
+ Or install your local checkout as the global CLI:
141
212
 
142
- ```text
143
- [2026-05-07T08:23:45.123Z] [zk-bridge] data dir: ~/.local/share/zk-bridge
213
+ ```bash
214
+ npm install -g .
215
+ zk-bridge start
144
216
  ```
145
217
 
146
- ## Auto-start on Host
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): *System → Auto-start on boot* in the web UI registers a systemd unit / Windows Scheduled Task / launchd plist with `Restart=on-failure`.
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 state.
230
+ - **Docker** — see [`docker-compose.yml`](docker-compose.yml). Bind-mount `./data:/app/data` to persist SQLite across rebuilds.
159
231
 
160
- Don't enable two methods at once — they'll fight for port 7000.
232
+ ## Environment Variables
161
233
 
162
- ## Self-upgrade
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
- ```bash
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
- `zk-bridge upgrade` runs `npm install -g <pkg>@latest` under the hood. Restart the host service afterwards so the running process picks up new code.
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 from this host. `curl <url>` from the host should work. |
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
- ## Develop from source
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
- ```bash
201
- npm install -g .
202
- zk-bridge start
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 &copy; [HanoiLab](mailto:opencode@hanoilab.vn)
283
+
284
+ ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanoilab/zk-bridge",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "private": false,
5
5
  "description": "Pull-side bridge for ZKTeco attendance devices — polls over LAN and pushes events to any HTTP backend",
6
6
  "keywords": [