@hanoilab/zk-bridge 0.1.4 → 0.1.6

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 CHANGED
@@ -1,43 +1,133 @@
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
+ ```
10
54
 
11
- # Start the bridge
55
+ ### 2. Run it
56
+
57
+ ```bash
12
58
  zk-bridge start
13
59
  ```
14
60
 
15
- That's it. You'll see:
16
-
17
61
  ```text
18
- [zk-bridge] data dir: ~/.local/share/zk-bridge
19
- [zk-bridge] UI listening on http://127.0.0.1:7000
20
- [scheduler] starting cron "*/5 * * * *" (every 5 min)
62
+ [zk-bridge] started (PID 12345)
63
+ Logs: ~/.local/share/zk-bridge/zk-bridge.log
64
+ Stop: zk-bridge stop
65
+ Tail: zk-bridge logs -f
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
+ `start` detaches closing the terminal won't stop the bridge. Useful one-liners:
24
69
 
25
- ## Features
70
+ ```bash
71
+ zk-bridge status # is it running?
72
+ zk-bridge logs -f # follow the log
73
+ zk-bridge stop # stop it
74
+ ```
75
+
76
+ ### 3. Open the admin UI
26
77
 
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
78
+ Visit **<http://localhost:7000>**, set the backend Push URL, paste the per-device JWT bridge pushes attendance on the next cycle.
35
79
 
36
- ## CLI Commands
80
+ ## Features
81
+
82
+ ### Web Admin UI
83
+ - Single-user login (bcrypt + signed-cookie session, 7-day TTL)
84
+ - Configure backend Push / Ping URL + poll interval
85
+ - Add devices manually or via LAN scan
86
+ - Per-device events with cursor position + push status badge
87
+ - Cycle history with status, timing, error message, filter by device
88
+
89
+ ### LAN Discovery
90
+ - Auto-scan `/24` subnet for ZKTeco devices on TCP 4370
91
+ - Identify each candidate over the ZK protocol (model, serial)
92
+ - One-click "Add as device" pre-fills name + host
93
+
94
+ ### Multi-Device
95
+ - One bridge polls many devices on a single schedule
96
+ - Per-device cursor, queue, audit log, error state
97
+ - Enable / disable individually without removing config
98
+
99
+ ### Offline Tolerance
100
+ - Queues events to local SQLite when the backend is unreachable
101
+ - Drains the queue on the next online cycle
102
+ - Cursor advances even on partial failure — no data loss, no double-send
103
+
104
+ ### Idempotent Push
105
+ - Backend dedupes by `(deviceId, eventLogId)` — replay is a no-op
106
+ - Batches events at 200/request to stay under common body-parser limits
107
+ - JWT version counter for revocation (regenerate on the backend → old JWTs rejected immediately)
108
+
109
+ ### Auto-Start on Boot
110
+ - One-click toggle in the System page registers a:
111
+ - **systemd** unit (Linux)
112
+ - **Scheduled Task** (Windows)
113
+ - **launchd** plist (macOS)
114
+ - `Restart=on-failure` so a crashed cycle never takes the bridge down
115
+
116
+ ### Cross-platform
117
+ - Linux, macOS, Windows
118
+ - Node 20+ — no native build needed (sqlite3 ships prebuilds)
119
+
120
+ ## CLI
37
121
 
38
122
  ```bash
39
- zk-bridge start # Start bridge (UI + scheduler) — default
40
- zk-bridge poll-once # One cycle then exit
123
+ zk-bridge start # Start as background daemon (writes PID + log)
124
+ zk-bridge stop # Stop the daemon
125
+ zk-bridge restart # Stop + start
126
+ zk-bridge status # PID, uptime, log path
127
+ zk-bridge logs -f # Follow the log
128
+ zk-bridge logs -n 200 # Last 200 lines
129
+ zk-bridge run # Run in the foreground (debug / systemd / Docker)
130
+ zk-bridge poll-once # Run a single cycle then exit
41
131
  zk-bridge reset-user # Forgot-password recovery
42
132
  zk-bridge recent-events # Print last N events from a device
43
133
  zk-bridge upgrade [tag] # Self-update via npm
@@ -45,28 +135,17 @@ zk-bridge --help
45
135
  zk-bridge --version
46
136
  ```
47
137
 
48
- ## Admin Panel
138
+ `start` detaches from the launching shell — `Ctrl+C` in that terminal does NOT kill the daemon. Use `zk-bridge stop`. Logs go to `<DATA_DIR>/zk-bridge.log`.
49
139
 
50
- After `zk-bridge start`, open `http://localhost:7000` to:
140
+ Environment overrides (otherwise default):
51
141
 
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
142
+ ```bash
143
+ PORT=8080 BIND_HOST=0.0.0.0 zk-bridge start
144
+ DATA_DIR=/var/lib/zk-bridge zk-bridge start
145
+ ```
57
146
 
58
147
  ## How It Works
59
148
 
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
149
  Every cycle (default 5 min):
71
150
 
72
151
  1. List enabled devices in local SQLite.
@@ -74,20 +153,32 @@ Every cycle (default 5 min):
74
153
  3. Drain the offline queue, then push new events to the backend in batches of 200.
75
154
  4. Advance the cursor, write a `cycle_log` row.
76
155
 
77
- Backend dedupes by `(deviceId, eventLogId)` — replays are safe.
156
+ State that lives locally:
157
+
158
+ ```
159
+ ~/.local/share/zk-bridge/zk-bridge.db (Linux default)
160
+ %APPDATA%\zk-bridge\zk-bridge.db (Windows)
161
+ ~/Library/Application Support/zk-bridge/zk-bridge.db (macOS)
162
+
163
+ ├── users (single admin row)
164
+ ├── config (push URL, ping URL, poll interval, session secret)
165
+ ├── devices (host, port, JWT, cursor, last status)
166
+ ├── event_queue (offline-pending events)
167
+ └── cycle_log (per-device cycle history, rotated to last 1000)
168
+ ```
78
169
 
79
170
  ## Backend Contract
80
171
 
81
- The bridge POSTs to **two URLs** you configure:
172
+ Two HTTP endpoints. Configure their full URLs in the UI — the bridge appends nothing.
82
173
 
83
- **Push URL** (required)
174
+ ### Push (required)
84
175
 
85
176
  ```http
86
177
  POST <push-url>
87
178
  Content-Type: application/json
88
179
 
89
180
  {
90
- "token": "<JWT>",
181
+ "token": "<JWT signed by backend>",
91
182
  "events": [
92
183
  {
93
184
  "eventLogId": "12345",
@@ -99,7 +190,16 @@ Content-Type: application/json
99
190
  }
100
191
  ```
101
192
 
102
- **Ping URL** (optional — used by the *Connect* button)
193
+ The backend should:
194
+
195
+ - Verify the JWT (signature + expiry / version).
196
+ - Resolve the device row from the JWT payload.
197
+ - Dedupe by `(deviceId, eventLogId)` — replays are safe.
198
+ - Persist or normalize the events as needed.
199
+
200
+ Response shape isn't enforced — bridge only checks the HTTP status (2xx = success, anything else = retry / queue).
201
+
202
+ ### Ping (optional)
103
203
 
104
204
  ```http
105
205
  POST <ping-url>
@@ -108,100 +208,93 @@ Content-Type: application/json
108
208
  { "token": "<JWT>" }
109
209
  ```
110
210
 
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
211
+ 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").
116
212
 
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)
213
+ ### Reference implementation
120
214
 
121
- ## Configuration
215
+ 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.
122
216
 
123
- Settings live in a local SQLite DB and are edited through the web UI. Only paths and bind come from env:
217
+ ## Self-Hosting
124
218
 
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 |
130
-
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`
219
+ ```bash
220
+ git clone https://github.com/nguyendinhphongdx/zkteco-bridge.git
221
+ cd zkteco-bridge
222
+ pnpm install
223
+ pnpm build
224
+ pnpm start
225
+ ```
139
226
 
140
- Boot prints the chosen path:
227
+ Or install your local checkout as the global CLI:
141
228
 
142
- ```text
143
- [2026-05-07T08:23:45.123Z] [zk-bridge] data dir: ~/.local/share/zk-bridge
229
+ ```bash
230
+ npm install -g .
231
+ zk-bridge start
144
232
  ```
145
233
 
146
- ## Auto-start on Host
234
+ ### Auto-start (production)
147
235
 
148
- Three options — pick one:
236
+ Three options — pick **one**, otherwise two processes will fight for port 7000:
149
237
 
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**:
238
+ - **Built-in toggle** (recommended) *System → Auto-start on boot* registers a systemd / Windows Task / launchd entry pointing at the global CLI binary.
239
+ - **PM2** —
152
240
 
153
241
  ```bash
154
242
  pm2 start "$(which zk-bridge)" --name zk-bridge -- start
155
243
  pm2 startup && pm2 save
156
244
  ```
157
245
 
158
- - **Docker** — see [`docker-compose.yml`](docker-compose.yml). Bind-mount `./data:/app/data` to persist state.
246
+ - **Docker** — see [`docker-compose.yml`](docker-compose.yml). Bind-mount `./data:/app/data` to persist SQLite across rebuilds.
159
247
 
160
- Don't enable two methods at once — they'll fight for port 7000.
248
+ ## Environment Variables
161
249
 
162
- ## Self-upgrade
250
+ | Variable | Default | Description |
251
+ |----------|---------|-------------|
252
+ | `PORT` | `7000` | Admin UI HTTP port |
253
+ | `BIND_HOST` | `127.0.0.1` | Listen address. Set `0.0.0.0` to allow LAN access |
254
+ | `DATA_DIR` | OS-standard user data dir | Override SQLite + admin login location |
255
+ | `PUSH_URL` | _(none)_ | First-run seed only — bridge stores it in SQLite then ignores env |
256
+ | `PING_URL` | _(none)_ | First-run seed only |
257
+ | `POLL_INTERVAL_MIN` | `5` | First-run seed only — minutes between cycles |
163
258
 
164
- ```bash
165
- zk-bridge upgrade
166
- sudo systemctl restart zk-bridge # or pm2 restart zk-bridge / docker compose pull
167
- ```
259
+ `DATA_DIR` resolution order on every start:
168
260
 
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.
261
+ 1. `DATA_DIR` env var (always wins)
262
+ 2. `./data/` next to cwd, if it exists (Docker bind mount, dev workflow)
263
+ 3. **Globally installed:** OS-standard user data dir
264
+ 4. `./data/` next to cwd (dev fallback)
170
265
 
171
266
  ## Troubleshooting
172
267
 
173
268
  | 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. |
269
+ |---------|-------------------|
270
+ | `ETIMEDOUT <ip>:<port>` on Connect | Bridge can't reach the backend Push URL. `curl <url>` from the bridge host should work. |
271
+ | `HTTP 401 Invalid token` | JWT was regenerated on the backend, or the device row was deleted. Re-paste the token. |
177
272
  | `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. |
273
+ | `port open but ZK probe fail` in scan | Same — device is busy. Try **Add****Connect** after a minute. |
179
274
  | 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). |
275
+ | Events arrive late | Lower the *poll interval* in *API settings* (min 1 min). |
181
276
 
182
- Every console line is prefixed with an ISO timestamp:
277
+ Every console line is prefixed with an ISO timestamp so logs from `pm2 logs` / `journalctl -u zk-bridge` / `docker compose logs` line up:
183
278
 
184
279
  ```text
185
280
  [2026-05-07T08:23:50.747Z] [poll] "Front gate" pulled 2915 from ZK in 5291ms
186
281
  ```
187
282
 
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:
283
+ ## Tech Stack
199
284
 
200
- ```bash
201
- npm install -g .
202
- zk-bridge start
203
- ```
285
+ | Layer | Technology |
286
+ |-------|-----------|
287
+ | Runtime | [Node.js 20+](https://nodejs.org/) |
288
+ | Local DB | [SQLite](https://www.sqlite.org/) via [sqlite3](https://github.com/TryGhost/node-sqlite3) + [Sequelize](https://sequelize.org/) |
289
+ | HTTP server | [Hono](https://hono.dev/) + [@hono/node-server](https://github.com/honojs/node-server) |
290
+ | ZK protocol | Custom client (`src/zklib`) — TCP raw, chunked streaming for large logs |
291
+ | Auth | [bcryptjs](https://github.com/dcodeIO/bcrypt.js) (admin) + JWT (per-device) |
292
+ | HTTP client | [axios](https://axios-http.com/) |
293
+ | Scheduler | [node-cron](https://github.com/node-cron/node-cron) |
294
+ | Build | [TypeScript](https://www.typescriptlang.org/) |
204
295
 
205
296
  ## License
206
297
 
207
- MIT
298
+ MIT &copy; [HanoiLab](mailto:opencode@hanoilab.vn)
299
+
300
+ ---
@@ -0,0 +1,229 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.startDaemon = startDaemon;
37
+ exports.stopDaemon = stopDaemon;
38
+ exports.statusDaemon = statusDaemon;
39
+ exports.tailLog = tailLog;
40
+ exports.formatUptime = formatUptime;
41
+ const node_child_process_1 = require("node:child_process");
42
+ const fs = __importStar(require("node:fs"));
43
+ const path = __importStar(require("node:path"));
44
+ const env_1 = require("../config/env");
45
+ const PID_FILE = 'zk-bridge.pid';
46
+ const LOG_FILE = 'zk-bridge.log';
47
+ function paths() {
48
+ const { dataDir } = (0, env_1.loadBootEnv)();
49
+ fs.mkdirSync(dataDir, { recursive: true });
50
+ return {
51
+ dataDir,
52
+ pid: path.join(dataDir, PID_FILE),
53
+ log: path.join(dataDir, LOG_FILE),
54
+ };
55
+ }
56
+ function isAlive(pid) {
57
+ try {
58
+ process.kill(pid, 0);
59
+ return true;
60
+ }
61
+ catch {
62
+ return false;
63
+ }
64
+ }
65
+ function readPid() {
66
+ const p = paths();
67
+ if (!fs.existsSync(p.pid))
68
+ return null;
69
+ const n = parseInt(fs.readFileSync(p.pid, 'utf8').trim(), 10);
70
+ if (!Number.isFinite(n) || !isAlive(n)) {
71
+ try {
72
+ fs.unlinkSync(p.pid);
73
+ }
74
+ catch {
75
+ // best-effort
76
+ }
77
+ return null;
78
+ }
79
+ return n;
80
+ }
81
+ /**
82
+ * Spawn the bridge as a detached background child writing to a log file.
83
+ * Parent exits immediately — Ctrl+C in the launching shell does NOT kill
84
+ * the daemon (it's in a separate process group).
85
+ */
86
+ function startDaemon() {
87
+ const existing = readPid();
88
+ if (existing) {
89
+ throw new Error(`Already running (PID ${existing}). Use 'zk-bridge stop' or 'zk-bridge restart' first.`);
90
+ }
91
+ const p = paths();
92
+ const fd = fs.openSync(p.log, 'a');
93
+ const node = process.execPath;
94
+ // dist/index.js — same entry `zk-bridge run` invokes, but spawned detached.
95
+ const entry = path.resolve(__dirname, '..', 'index.js');
96
+ const child = (0, node_child_process_1.spawn)(node, [entry], {
97
+ detached: true,
98
+ stdio: ['ignore', fd, fd],
99
+ env: process.env,
100
+ });
101
+ child.unref();
102
+ if (!child.pid)
103
+ throw new Error('Failed to spawn child process.');
104
+ fs.writeFileSync(p.pid, String(child.pid));
105
+ fs.closeSync(fd);
106
+ return { pid: child.pid, logPath: p.log, dataDir: p.dataDir };
107
+ }
108
+ function stopDaemon() {
109
+ const pid = readPid();
110
+ if (!pid)
111
+ return null;
112
+ try {
113
+ process.kill(pid, 'SIGTERM');
114
+ }
115
+ catch {
116
+ // already dead — fall through to PID cleanup
117
+ }
118
+ // Wait a moment for graceful shutdown, then verify.
119
+ const deadline = Date.now() + 5_000;
120
+ while (Date.now() < deadline && isAlive(pid)) {
121
+ // busy-wait short; the kill is async
122
+ sleepSync(100);
123
+ }
124
+ if (isAlive(pid)) {
125
+ try {
126
+ process.kill(pid, 'SIGKILL');
127
+ }
128
+ catch {
129
+ // ignore
130
+ }
131
+ }
132
+ const p = paths();
133
+ try {
134
+ fs.unlinkSync(p.pid);
135
+ }
136
+ catch {
137
+ // ignore
138
+ }
139
+ return { pid };
140
+ }
141
+ function statusDaemon() {
142
+ const p = paths();
143
+ const pid = readPid();
144
+ if (!pid) {
145
+ return { running: false, logPath: p.log, dataDir: p.dataDir };
146
+ }
147
+ let uptimeMs;
148
+ try {
149
+ const stat = fs.statSync(p.pid);
150
+ uptimeMs = Date.now() - stat.mtimeMs;
151
+ }
152
+ catch {
153
+ // ignore
154
+ }
155
+ return { running: true, pid, logPath: p.log, dataDir: p.dataDir, uptimeMs };
156
+ }
157
+ /**
158
+ * Print last N lines of the log file, optionally follow new appends.
159
+ * Cross-platform tail — doesn't shell out.
160
+ */
161
+ function tailLog(follow, lines) {
162
+ const p = paths();
163
+ if (!fs.existsSync(p.log)) {
164
+ console.log('(no log yet — daemon has not started.)');
165
+ return;
166
+ }
167
+ const initial = readLastLines(p.log, lines);
168
+ process.stdout.write(initial);
169
+ if (!follow)
170
+ return;
171
+ let pos = fs.statSync(p.log).size;
172
+ const watcher = fs.watch(p.log, () => {
173
+ let stat;
174
+ try {
175
+ stat = fs.statSync(p.log);
176
+ }
177
+ catch {
178
+ return;
179
+ }
180
+ if (stat.size > pos) {
181
+ const fd = fs.openSync(p.log, 'r');
182
+ const buf = Buffer.alloc(stat.size - pos);
183
+ fs.readSync(fd, buf, 0, buf.length, pos);
184
+ fs.closeSync(fd);
185
+ process.stdout.write(buf.toString('utf8'));
186
+ pos = stat.size;
187
+ }
188
+ else if (stat.size < pos) {
189
+ pos = 0;
190
+ }
191
+ });
192
+ process.on('SIGINT', () => {
193
+ watcher.close();
194
+ process.exit(0);
195
+ });
196
+ // Keep the event loop alive.
197
+ setInterval(() => undefined, 1 << 30);
198
+ }
199
+ function readLastLines(file, lines) {
200
+ const content = fs.readFileSync(file, 'utf8');
201
+ const arr = content.split('\n');
202
+ // Trim trailing empty line caused by final newline.
203
+ if (arr[arr.length - 1] === '')
204
+ arr.pop();
205
+ const slice = arr.slice(-lines);
206
+ return slice.join('\n') + (slice.length > 0 ? '\n' : '');
207
+ }
208
+ function formatUptime(ms) {
209
+ const s = Math.floor(ms / 1000);
210
+ if (s < 60)
211
+ return `${s}s`;
212
+ const m = Math.floor(s / 60);
213
+ if (m < 60)
214
+ return `${m}m ${s % 60}s`;
215
+ const h = Math.floor(m / 60);
216
+ if (h < 24)
217
+ return `${h}h ${m % 60}m`;
218
+ const d = Math.floor(h / 24);
219
+ return `${d}d ${h % 24}h`;
220
+ }
221
+ function sleepSync(ms) {
222
+ // Atomics.wait on a SharedArrayBuffer would be precise but heavy; this
223
+ // ~100ms granularity is fine for stop's grace-period polling.
224
+ const end = Date.now() + ms;
225
+ while (Date.now() < end) {
226
+ // spin
227
+ }
228
+ }
229
+ //# sourceMappingURL=daemon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"daemon.js","sourceRoot":"","sources":["../../src/cli/daemon.ts"],"names":[],"mappings":"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"}
@@ -44,13 +44,19 @@ var __importStar = (this && this.__importStar) || (function () {
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  const node_child_process_1 = require("node:child_process");
46
46
  const path = __importStar(require("node:path"));
47
+ const daemon_1 = require("./daemon");
47
48
  const command = process.argv[2];
48
49
  function help() {
49
50
  // eslint-disable-next-line no-console
50
51
  console.log(`Usage: zk-bridge <command> [options]
51
52
 
52
53
  Commands:
53
- start Start bridge (UI + scheduler) default if no command given
54
+ start Start bridge in the background (writes PID + log)
55
+ stop Stop the running daemon
56
+ restart Stop + start
57
+ status Show daemon state (PID, uptime, log path)
58
+ logs [-f] [-n N] Print last N log lines (default 50). -f to follow.
59
+ run Run in the foreground (debug / systemd / Docker)
54
60
  poll-once Run a single poll cycle then exit (no UI server)
55
61
  reset-user Reset the local admin user (forgot password recovery)
56
62
  recent-events Print recent events from a device
@@ -60,18 +66,18 @@ Commands:
60
66
  version, --version, -v Show package version
61
67
 
62
68
  Environment:
63
- DATA_DIR=./data Where SQLite + admin credentials live
69
+ DATA_DIR Where SQLite + admin credentials + log live
70
+ (default: OS user data dir)
64
71
  PORT=7000 UI HTTP port
65
72
  BIND_HOST=127.0.0.1 Bind address. Set 0.0.0.0 to allow LAN access
66
73
 
67
74
  Examples:
68
- zk-bridge start
69
- PORT=8080 zk-bridge start
70
- zk-bridge poll-once
71
- zk-bridge reset-user
72
- zk-bridge upgrade
73
- zk-bridge upgrade next
74
- zk-bridge recent-events --device "Cửa chính" -n 30
75
+ zk-bridge start # daemonize
76
+ zk-bridge status # is it running?
77
+ zk-bridge logs -f # follow logs
78
+ zk-bridge stop # stop the daemon
79
+ PORT=8080 zk-bridge run # foreground on a custom port
80
+ zk-bridge recent-events --device "Front gate" -n 30
75
81
  `);
76
82
  }
77
83
  function version() {
@@ -121,19 +127,87 @@ async function upgrade(tag = 'latest') {
121
127
  });
122
128
  // eslint-disable-next-line no-console
123
129
  console.log(`[zk-bridge] ✓ upgraded ${target}.\n` +
124
- 'Restart the running bridge process to pick up the new code:\n' +
125
- ' systemd: sudo systemctl restart zk-bridge\n' +
126
- ' Windows Task: schtasks /End /TN "ZK-Bridge (zk-bridge)" && schtasks /Run /TN "ZK-Bridge (zk-bridge)"\n' +
127
- ' launchd: launchctl kickstart -k gui/$UID/com.chr.zk-bridge\n' +
128
- ' pm2: pm2 restart zk-bridge\n' +
129
- ' docker compose: docker compose pull && docker compose up -d\n' +
130
- ' manual: Ctrl+C the foreground process, then `zk-bridge start` again');
130
+ 'Restart the daemon to pick up the new code:\n' +
131
+ ' zk-bridge restart');
132
+ }
133
+ function parseLogsArgs() {
134
+ const argv = process.argv.slice(3);
135
+ const follow = argv.includes('-f') || argv.includes('--follow');
136
+ const nIdx = argv.findIndex((a) => a === '-n' || a === '--lines');
137
+ const lines = nIdx >= 0 ? Number(argv[nIdx + 1]) : 50;
138
+ return { follow, lines: Number.isFinite(lines) && lines > 0 ? lines : 50 };
139
+ }
140
+ function cmdStart() {
141
+ try {
142
+ const r = (0, daemon_1.startDaemon)();
143
+ // eslint-disable-next-line no-console
144
+ console.log(`[zk-bridge] started (PID ${r.pid})\n` +
145
+ ` Logs: ${r.logPath}\n` +
146
+ ` Stop: zk-bridge stop\n` +
147
+ ` Tail: zk-bridge logs -f`);
148
+ }
149
+ catch (err) {
150
+ // eslint-disable-next-line no-console
151
+ console.error(`[zk-bridge] ${err instanceof Error ? err.message : err}`);
152
+ process.exit(1);
153
+ }
154
+ }
155
+ function cmdStop() {
156
+ const r = (0, daemon_1.stopDaemon)();
157
+ if (!r) {
158
+ // eslint-disable-next-line no-console
159
+ console.log('[zk-bridge] not running.');
160
+ return;
161
+ }
162
+ // eslint-disable-next-line no-console
163
+ console.log(`[zk-bridge] stopped (PID ${r.pid}).`);
164
+ }
165
+ async function cmdRestart() {
166
+ cmdStop();
167
+ await new Promise((r) => setTimeout(r, 600));
168
+ cmdStart();
169
+ }
170
+ function cmdStatus() {
171
+ const s = (0, daemon_1.statusDaemon)();
172
+ if (!s.running) {
173
+ // eslint-disable-next-line no-console
174
+ console.log(`[zk-bridge] not running.\n` +
175
+ ` Data dir: ${s.dataDir}\n` +
176
+ ` Logs: ${s.logPath} (last run, if any)`);
177
+ return;
178
+ }
179
+ const up = s.uptimeMs !== undefined ? (0, daemon_1.formatUptime)(s.uptimeMs) : 'unknown';
180
+ // eslint-disable-next-line no-console
181
+ console.log(`[zk-bridge] running.\n` +
182
+ ` PID: ${s.pid}\n` +
183
+ ` Uptime: ${up}\n` +
184
+ ` Data dir: ${s.dataDir}\n` +
185
+ ` Logs: ${s.logPath}`);
131
186
  }
132
187
  async function main() {
133
188
  switch (command) {
189
+ // ── daemon control ───────────────────────────────────────────────────
134
190
  case undefined:
135
191
  case 'start':
136
- // Strip the subcommand so the underlying entry sees a clean argv.
192
+ cmdStart();
193
+ return;
194
+ case 'stop':
195
+ cmdStop();
196
+ return;
197
+ case 'restart':
198
+ await cmdRestart();
199
+ return;
200
+ case 'status':
201
+ cmdStatus();
202
+ return;
203
+ case 'logs': {
204
+ const { follow, lines } = parseLogsArgs();
205
+ (0, daemon_1.tailLog)(follow, lines);
206
+ return;
207
+ }
208
+ // ── foreground / one-shot ────────────────────────────────────────────
209
+ case 'run':
210
+ // Foreground mode — what daemons / Docker / systemd should call.
137
211
  process.argv.splice(2, 1);
138
212
  await Promise.resolve().then(() => __importStar(require('../index')));
139
213
  return;
@@ -149,11 +223,12 @@ async function main() {
149
223
  process.argv.splice(2, 1);
150
224
  await Promise.resolve().then(() => __importStar(require('./recent-events')));
151
225
  return;
226
+ // ── self-update ──────────────────────────────────────────────────────
152
227
  case 'upgrade':
153
228
  case 'update':
154
- // Optional next arg = npm dist-tag (e.g. `next`). Default `latest`.
155
229
  await upgrade(process.argv[3] ?? 'latest');
156
230
  return;
231
+ // ── meta ─────────────────────────────────────────────────────────────
157
232
  case 'help':
158
233
  case '--help':
159
234
  case '-h':
@@ -1 +1 @@
1
- {"version":3,"file":"zk-bridge.js","sourceRoot":"","sources":["../../src/cli/zk-bridge.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"zk-bridge.js","sourceRoot":"","sources":["../../src/cli/zk-bridge.ts"],"names":[],"mappings":"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"}
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.6",
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": [