@savepoint/bridge 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) SavePoint
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # SavePoint Bridge
2
+
3
+ Connect your local printers and hardware to SavePoint.
4
+
5
+ SavePoint Bridge is a small background service that runs on your local network. It receives print jobs from SavePoint (labels, receipts) and sends them directly to your physical printers — no cloud-to-printer connection required.
6
+
7
+ ---
8
+
9
+ ## Requirements
10
+
11
+ - **Node.js 18+** — [nodejs.org](https://nodejs.org)
12
+ - **macOS, Linux, or Windows**
13
+ - A network printer on port 9100, or a printer exposed through CUPS
14
+ - A SavePoint account with the Bridge feature enabled
15
+
16
+ ---
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ npm install -g @savepoint/bridge
22
+ ```
23
+
24
+ Or run without installing:
25
+
26
+ ```bash
27
+ npx @savepoint/bridge setup
28
+ ```
29
+
30
+ ---
31
+
32
+ ## Setup
33
+
34
+ The setup wizard walks you through everything in about 5 minutes.
35
+
36
+ **Step 1 — Get your agent token**
37
+
38
+ 1. In SavePoint, go to **Settings > Bridge > Add Bridge**
39
+ 2. Copy the token shown
40
+
41
+ **Step 2 — Run setup**
42
+
43
+ ```bash
44
+ savepoint-bridge setup
45
+ ```
46
+
47
+ The wizard will:
48
+ - Verify your token
49
+ - Scan your network and local CUPS queues for printers
50
+ - Install the bridge as a background service (starts automatically on login/boot)
51
+
52
+ **That's it.** Print a test label from **Settings > Printers** to confirm everything works.
53
+
54
+ ---
55
+
56
+ ## Commands
57
+
58
+ ```
59
+ savepoint-bridge setup Guided setup wizard (start here)
60
+ savepoint-bridge start Start the daemon manually (without installing as a service)
61
+ savepoint-bridge status Show running state, connected printers, and recent jobs
62
+ savepoint-bridge logs View recent activity
63
+ savepoint-bridge discover Re-scan for printers on your network and local CUPS queues
64
+ savepoint-bridge uninstall Remove the background service and stored token
65
+ ```
66
+
67
+ ---
68
+
69
+ ## Background Service
70
+
71
+ The setup wizard installs the bridge as an OS-level service so it starts automatically.
72
+
73
+ | OS | Service type |
74
+ |----|-------------|
75
+ | macOS | LaunchAgent (`~/Library/LaunchAgents/com.savepoint.bridge.plist`) |
76
+ | Linux | systemd user unit (`~/.config/systemd/user/savepoint-bridge.service`) |
77
+ | Windows | Scheduled Task at logon ("SavePoint Bridge") |
78
+
79
+ To stop and remove the service:
80
+
81
+ ```bash
82
+ savepoint-bridge uninstall
83
+ ```
84
+
85
+ ---
86
+
87
+ ## Automation / Silent Setup
88
+
89
+ For IT teams deploying to multiple machines:
90
+
91
+ ```bash
92
+ savepoint-bridge setup \
93
+ --token=<your-agent-token> \
94
+ --non-interactive \
95
+ --no-service
96
+ ```
97
+
98
+ | Flag | Description |
99
+ |------|-------------|
100
+ | `--token=<token>` | Skip the token prompt |
101
+ | `--non-interactive` | Suppress all prompts, use defaults |
102
+ | `--no-service` | Do not install as an OS service |
103
+ | `--log-level=debug` | Verbose output |
104
+
105
+ ---
106
+
107
+ ## What It Stores on Your Computer
108
+
109
+ | Data | Location |
110
+ |------|---------|
111
+ | Agent token | OS keychain (macOS Keychain, Windows Credential Manager, or gnome-keyring) |
112
+ | Token fallback | `~/.savepoint-bridge/.token` (AES-256 encrypted file with machine-local derived key) |
113
+ | Printer registry | `~/.savepoint-bridge/registry.db` |
114
+ | Log files | `~/.savepoint-bridge/logs/` (7-day rolling) |
115
+
116
+ The agent token and print job content are never written to log files.
117
+
118
+ ---
119
+
120
+ ## Security
121
+
122
+ - **Outbound only.** The bridge polls `api.savepointhq.com` — no inbound ports are opened, no firewall changes required.
123
+ - **Tenant-scoped.** The agent token only has access to print jobs and printers for your store. It cannot read orders, customers, or any non-print data.
124
+ - **Revocable.** Go to **Settings > Bridge > Revoke** at any time. The bridge stops immediately on its next poll.
125
+ - **TLS always on.** Certificate validation is always enforced. If your network uses an SSL-intercepting proxy, set `NODE_EXTRA_CA_CERTS=/path/to/proxy-ca.crt` before running the bridge.
126
+
127
+ ---
128
+
129
+ ## Supported Printers
130
+
131
+ ### Pre-configured models
132
+
133
+ | Printer | Format | Connection |
134
+ |---------|--------|-----------|
135
+ | Zebra ZT230 | ZPL | Network / CUPS |
136
+ | Zebra ZT410 | ZPL | Network / CUPS |
137
+ | Zebra GK420d | ZPL | Network / CUPS |
138
+ | Epson TM-T88 | ESC/P | Network / CUPS |
139
+ | Epson TM-T20 | ESC/P | Network / CUPS |
140
+ | Brother QL-820NWB | RAW | Network / CUPS |
141
+
142
+ Any printer not in this list can be added manually in **Settings > Printers** with a custom host/port.
143
+
144
+ ### Connection types
145
+
146
+ - **Network** — TCP socket to port 9100 (raw ZPL / ESC/P / JetDirect). Most common.
147
+ - **System (CUPS)** — Uses the OS print queue. macOS and Linux.
148
+
149
+ USB transport is not public in `1.0.0`; use network or CUPS-backed printers for the first public release.
150
+
151
+ ---
152
+
153
+ ## Troubleshooting
154
+
155
+ **Bridge doesn't connect after setup**
156
+
157
+ - Check that the machine running the bridge can reach `api.savepointhq.com` on port 443
158
+ - Run `savepoint-bridge logs` to see error details
159
+ - Run `savepoint-bridge status` to confirm the service is running
160
+
161
+ **Printer not found during setup**
162
+
163
+ - Make sure the printer is powered on and connected to the same network
164
+ - For network printers: verify the printer's IP is reachable (`ping <printer-ip>`)
165
+ - For CUPS printers: confirm the queue exists locally with `lpstat -p`
166
+ - You can always add a printer manually in **Settings > Printers** after setup
167
+
168
+ **"Token verification failed"**
169
+
170
+ - The token is single-use display — copy it fresh from **Settings > Bridge > Add Bridge**
171
+ - Check that the token is complete (64 hex characters)
172
+
173
+ **SSL / certificate error**
174
+
175
+ Your network may use an SSL-intercepting proxy. Run:
176
+
177
+ ```bash
178
+ NODE_EXTRA_CA_CERTS=/path/to/your-proxy-ca.crt savepoint-bridge setup
179
+ ```
180
+
181
+ Or add the certificate to your system's trust store and restart the bridge.
182
+
183
+ **Linux: bridge doesn't start at boot on a headless machine**
184
+
185
+ The setup wizard runs `loginctl enable-linger $USER` automatically. If you skipped setup or it failed, run:
186
+
187
+ ```bash
188
+ loginctl enable-linger $(whoami)
189
+ systemctl --user enable --now savepoint-bridge
190
+ ```
191
+
192
+ ---
193
+
194
+ ## Logs
195
+
196
+ Logs are structured JSON written to `~/.savepoint-bridge/logs/YYYY-MM-DD.log`. The `logs` command renders them in a human-readable format:
197
+
198
+ ```bash
199
+ savepoint-bridge logs
200
+ ```
201
+
202
+ Logs rotate automatically — only the last 7 days are kept.
203
+
204
+ ---
205
+
206
+ ## Uninstall
207
+
208
+ ```bash
209
+ savepoint-bridge uninstall
210
+ ```
211
+
212
+ This removes the OS service and the stored token. It does not remove the `@savepoint/bridge` npm package itself — run `npm uninstall -g @savepoint/bridge` to remove that too.
package/USAGE.md ADDED
@@ -0,0 +1,366 @@
1
+ # SavePoint Bridge — Developer Reference
2
+
3
+ Internal reference for the SavePoint engineering team. For retailer/IT setup, see [README.md](./README.md).
4
+
5
+ ---
6
+
7
+ ## Architecture Overview
8
+
9
+ The bridge is a Node.js daemon in `apps/bridge` (`@savepoint/bridge`). It polls the SavePoint control-plane for queued print jobs, validates them against a local printer capability registry, and dispatches raw bytes to physical printers over TCP or CUPS.
10
+
11
+ ```
12
+ apps/bridge/
13
+ src/
14
+ cli.ts Entry point — parse args, route to setup or daemon
15
+ daemon.ts Main poll loop — adaptive interval, job dispatch, signal handling
16
+ poller.ts HTTP client — poll/claim jobs, report results, heartbeat
17
+ printer.ts Transport layer — TCP socket, CUPS pipe
18
+ registry.ts SQLite printer registry — seed, upsert, capability lookup
19
+ discovery.ts Network (port 9100 sweep) + CUPS printer discovery
20
+ token.ts OS keychain storage + AES-256-GCM encrypted file fallback
21
+ setup.ts Interactive setup wizard (@clack/prompts)
22
+ service.ts OS service install/uninstall (launchd/systemd/schtasks)
23
+ logger.ts Pretty terminal output + rolling JSON log files
24
+ capabilities/
25
+ printers.json Curated printer model → format → VID/PID map (ships with binary)
26
+ src/__tests__/
27
+ token.test.ts
28
+ registry.test.ts
29
+ poller.test.ts
30
+ printer.test.ts
31
+ daemon.test.ts
32
+
33
+ types.ts Local runtime-safe bridge contract types
34
+ ```
35
+
36
+ ---
37
+
38
+ ## Job Dispatch Flow
39
+
40
+ ```
41
+ Control plane Bridge daemon
42
+ ───────────── ─────────────
43
+ POST /api/labels/print-jobs poll GET /api/labels/print-jobs
44
+ status = 'queued' ──────────────► every 5s (active) / 30s (idle)
45
+
46
+ ◄────────────── POST /api/labels/print-jobs/:id/claim
47
+ → 409 already claimed → skip
48
+ → 200 claimed → proceed
49
+
50
+ registry.validateFormat(printer_id, job.metadata.format)
51
+ → FORMAT_MISMATCH → report failed, stop
52
+
53
+ fetch(content_url) ← R2 pre-signed URL
54
+ → retry ×2 with exponential backoff
55
+
56
+ getTransport(printer).send(bytes)
57
+ → TCP socket / CUPS lpr
58
+
59
+ ◄────────────── POST /api/labels/print-jobs/:id/result
60
+ { status: 'completed' | 'failed', result: {...} }
61
+
62
+ ◄────────────── POST /api/agents/heartbeat (every 60s)
63
+ ```
64
+
65
+ **Adaptive polling:** `buildIntervalStrategy` in `daemon.ts` — 5s when jobs were found on the last poll, backs off to 30s after 3 consecutive empty polls. Resets to 5s on the next non-empty poll.
66
+
67
+ **Claim safety:** The control-plane uses a conditional UPDATE (`WHERE status = 'queued' AND claimed_by IS NULL`). If two bridge instances race, the loser gets `409` and skips the job. No distributed lock needed.
68
+
69
+ ---
70
+
71
+ ## API Contract
72
+
73
+ All requests include:
74
+
75
+ ```
76
+ Authorization: Bearer <agent_token>
77
+ X-Agent-Version: <semver>
78
+ Content-Type: application/json
79
+ ```
80
+
81
+ ### Endpoints the bridge calls
82
+
83
+ | Method | Path | Description |
84
+ |--------|------|-------------|
85
+ | `GET` | `/api/labels/print-jobs` | Poll for work |
86
+ | `POST` | `/api/labels/print-jobs/:id/claim` | Claim a job |
87
+ | `POST` | `/api/labels/print-jobs/:id/result` | Report result |
88
+ | `POST` | `/api/agents/heartbeat` | Update `last_seen_at` and sync discovered printers |
89
+ | `GET` | `/api/agents/printers` | Refresh printer registry |
90
+
91
+ ### Claim a job
92
+
93
+ ```
94
+ POST /api/labels/print-jobs/:id/claim
95
+
96
+ 200 { job: PrintJob } — claimed, proceed
97
+ 409 — already claimed, skip
98
+ 404 — job gone, skip
99
+ 401 — token revoked, stop polling, prompt re-setup
100
+ ```
101
+
102
+ ### Report result
103
+
104
+ ```
105
+ POST /api/labels/print-jobs/:id/result
106
+ Body: { status: 'completed' | 'failed', result: JobResult }
107
+ ```
108
+
109
+ ### Heartbeat
110
+
111
+ ```
112
+ POST /api/agents/heartbeat
113
+ Body: { version?, printers?[] }
114
+ → 200 { ok: true }
115
+ ```
116
+
117
+ Sent every 60s. Full discovery sync runs on startup and every 5 minutes. Non-fatal if it fails — daemon continues.
118
+
119
+ ---
120
+
121
+ ## Shared Types (`src/types.ts`)
122
+
123
+ ```typescript
124
+ type JobType = 'label' | 'receipt' | 'document' | 'test'
125
+ type JobStatus = 'queued' | 'processing' | 'completed' | 'failed' | 'cancelled'
126
+
127
+ interface PrintJob {
128
+ id: string
129
+ tenant_id: string
130
+ printer_id: string | null
131
+ job_type: JobType
132
+ content_url: string | null // R2 pre-signed URL, expires after 1 hour
133
+ metadata: { format?: 'zpl' | 'esc-p' | 'pdf' | 'raw'; copies?: number; [k: string]: unknown }
134
+ status: JobStatus
135
+ claimed_by: string | null // agents.id UUID
136
+ claimed_at: string | null
137
+ created_at: string
138
+ updated_at: string
139
+ }
140
+
141
+ interface JobResult {
142
+ code?: string
143
+ message?: string
144
+ attempts?: number
145
+ http_status?: number
146
+ [key: string]: unknown
147
+ }
148
+
149
+ type ConnectionType = 'network' | 'usb' | 'system'
150
+
151
+ interface Printer {
152
+ id: string; name: string; model: string | null
153
+ host: string | null; port: number | null
154
+ connectionType: ConnectionType; protocol: string | null; isDefault: boolean
155
+ }
156
+
157
+ interface PrinterCapability {
158
+ model: string; manufacturer: string
159
+ expected_format: 'zpl' | 'esc-p' | 'pdf' | 'raw'
160
+ default_port: number; connection_types: ConnectionType[]
161
+ vid?: number; pid?: number // USB Vendor/Product ID
162
+ }
163
+
164
+ ```
165
+
166
+ ---
167
+
168
+ ## Token Storage
169
+
170
+ **Primary:** OS keychain via `@postman/node-keytar` (actively maintained fork of archived `keytar`).
171
+
172
+ **Fallback** (headless Linux without gnome-keyring):
173
+ - AES-256-GCM encrypted file at `~/.savepoint-bridge/.token`
174
+ - Key derived from `SHA-256(HOME_DIR + ":" + platform)` — machine-stable, not portable
175
+ - Setup wizard displays a warning when fallback is used
176
+
177
+ `@postman/node-keytar` is used over the original `keytar` (archived 2023, build failures on Node 20+ Linux).
178
+
179
+ ---
180
+
181
+ ## Printer Capability Registry
182
+
183
+ Local SQLite database (`~/.savepoint-bridge/registry.db`) seeded from two sources:
184
+
185
+ 1. **`capabilities/printers.json`** — curated `model → { manufacturer, expected_format, default_port, vid, pid }` map, shipped with the binary
186
+ 2. **Control-plane** — `GET /api/agents/printers` on startup and every 5 minutes
187
+
188
+ `Registry.validateFormat(printerId, jobFormat)` returns `null` (pass) or a `RegistryError` with `code: FORMAT_MISMATCH | PRINTER_NOT_FOUND`. Format mismatches are caught before any bytes are sent.
189
+
190
+ `better-sqlite3` is synchronous by design — the registry lookup is in the hot path of every job, and the sync API simplifies the dispatch code without performance concerns at this scale.
191
+
192
+ ---
193
+
194
+ ## Error Codes
195
+
196
+ | Code | Cause | Outcome |
197
+ |------|-------|---------|
198
+ | `FORMAT_MISMATCH` | Job format ≠ printer expected format | Job failed, no bytes sent |
199
+ | `PRINTER_NOT_FOUND` | `printer_id` not in local registry | Job failed |
200
+ | `NO_CONTENT_URL` | Job has no `content_url` | Job failed |
201
+ | `DOWNLOAD_FAILED` | R2 fetch non-200 or network error | Retry ×2, then job failed |
202
+ | `CONNECTION_FAILED` | TCP timeout/refused | Retry ×3 with backoff, then job failed |
203
+ | `DISPATCH_ERROR` | Any unhandled transport error | Job failed |
204
+ | `UNAUTHORIZED` (401) | Token revoked | Stop polling, log notice, exit |
205
+
206
+ **Content URL expiry:** R2 pre-signed URLs expire after 1 hour. Jobs queued but not processed within that window will fail at download with `DOWNLOAD_FAILED`. The platform should set pre-signed URL expiry to cover expected queue depth.
207
+
208
+ ---
209
+
210
+ ## OS Service Installation
211
+
212
+ Runs as a **user-level service** on all platforms (not root/SYSTEM) to get correct `$HOME` paths and user-level printer access.
213
+
214
+ | OS | Mechanism | Path |
215
+ |----|-----------|------|
216
+ | macOS | launchd LaunchAgent | `~/Library/LaunchAgents/com.savepoint.bridge.plist` |
217
+ | Linux | systemd user unit + `loginctl enable-linger` | `~/.config/systemd/user/savepoint-bridge.service` |
218
+ | Windows | Scheduled Task at logon | Task Scheduler: "SavePoint Bridge" |
219
+
220
+ `loginctl enable-linger $USER` is set on Linux so the user unit survives logout — required for headless back-of-house machines.
221
+
222
+ All shell commands in `service.ts` and `discovery.ts` use `execFile()` with array arguments to prevent shell injection. No user-controlled data is ever interpolated into command strings.
223
+
224
+ ---
225
+
226
+ ## Local Storage Paths
227
+
228
+ | Data | macOS / Linux | Windows |
229
+ |------|--------------|---------|
230
+ | Token (keychain) | OS keychain | Windows Credential Manager |
231
+ | Token (fallback) | `~/.savepoint-bridge/.token` | `%APPDATA%\savepoint-bridge\.token` |
232
+ | Printer registry | `~/.savepoint-bridge/registry.db` | `%APPDATA%\savepoint-bridge\registry.db` |
233
+ | Logs | `~/.savepoint-bridge/logs/YYYY-MM-DD.log` | `%APPDATA%\savepoint-bridge\logs\` |
234
+
235
+ ---
236
+
237
+ ## Development
238
+
239
+ ### Setup
240
+
241
+ ```bash
242
+ # From repo root
243
+ pnpm install
244
+
245
+ # Run in dev mode (tsx, no build needed)
246
+ pnpm --filter @savepoint/bridge dev -- --help
247
+ pnpm --filter @savepoint/bridge dev -- setup --non-interactive --no-service --token=<64-char-token>
248
+ ```
249
+
250
+ ### Test
251
+
252
+ ```bash
253
+ # From apps/bridge (recommended — avoids cross-workspace runner conflicts)
254
+ cd apps/bridge
255
+ pnpm test # All tests
256
+ pnpm test token # Single suite
257
+ pnpm test:watch # Watch mode
258
+ ```
259
+
260
+ Tests are TDD-structured: each module has a corresponding `src/__tests__/*.test.ts` with mocked I/O. The daemon loop (`daemon.ts`) exports `buildIntervalStrategy` as a pure function specifically to allow unit testing the adaptive interval logic in isolation from the live poll loop.
261
+
262
+ ### Build
263
+
264
+ ```bash
265
+ cd apps/bridge
266
+ pnpm build # Compiles to dist/
267
+ pnpm type-check # tsc --noEmit only
268
+ ```
269
+
270
+ ### Test the built binary
271
+
272
+ ```bash
273
+ node apps/bridge/dist/cli.js --help
274
+ ```
275
+
276
+ `dist/` is gitignored — do not commit build artifacts.
277
+
278
+ ---
279
+
280
+ ## Key Dependencies
281
+
282
+ | Package | Purpose | Notes |
283
+ |---------|---------|-------|
284
+ | `better-sqlite3@^12` | Printer capability registry | Synchronous API intentional; v9 does not support Node 25 |
285
+ | `@postman/node-keytar` | OS keychain access | Active fork of archived `keytar`; identical API |
286
+ | `@clack/prompts` | Interactive wizard UI | Polished terminal prompts |
287
+ | `picocolors` | Terminal colour | Zero-dependency |
288
+ | `tsx` | Dev runtime (no build step) | devDependency only |
289
+
290
+ USB transport (`createUsbTransport` in `printer.ts`) is a deferred post-1.0 task — it throws immediately and is not part of the first public release.
291
+
292
+ ---
293
+
294
+ ## v2 Extension Points
295
+
296
+ The registry/transport split is designed for transcoding without a rewrite:
297
+
298
+ 1. Add a `transcoder` field to `capabilities/printers.json` per model family
299
+ 2. `registry.ts` selects the transcoder module for the resolved model
300
+ 3. Transcoder modules are dynamically imported in `daemon.ts` before `transport.send()`
301
+ 4. Transport layer is unchanged
302
+
303
+ Purely additive — job schema and API contract do not change.
304
+
305
+ ---
306
+
307
+ ## Database Schema (relevant tables)
308
+
309
+ Defined in `neon/migrations/070_print_jobs.sql` and `071_agents.sql`.
310
+
311
+ ```sql
312
+ -- print_jobs
313
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid()
314
+ tenant_id UUID NOT NULL REFERENCES tenants(id)
315
+ printer_id UUID REFERENCES printers(id) ON DELETE SET NULL
316
+ job_type TEXT CHECK (job_type IN ('label','receipt','document','test'))
317
+ content_url TEXT -- R2 pre-signed URL, expires 1 hour
318
+ metadata JSONB DEFAULT '{}'
319
+ status TEXT DEFAULT 'queued'
320
+ result JSONB
321
+ claimed_by UUID REFERENCES agents(id) ON DELETE SET NULL
322
+ claimed_at TIMESTAMPTZ
323
+ created_at / updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
324
+
325
+ -- agents (bridge instance registry)
326
+ id UUID PRIMARY KEY
327
+ tenant_id UUID NOT NULL REFERENCES tenants(id)
328
+ external_agent_id TEXT -- machine identifier sent at registration (not the claim value)
329
+ name TEXT NOT NULL
330
+ version TEXT
331
+ last_seen_at TIMESTAMPTZ
332
+ created_at / updated_at / deleted_at TIMESTAMPTZ
333
+
334
+ -- agent_tokens (hashed token store)
335
+ id UUID PRIMARY KEY
336
+ agent_id UUID NOT NULL REFERENCES agents(id)
337
+ tenant_id UUID NOT NULL
338
+ token_hash TEXT NOT NULL UNIQUE -- SHA-256(raw_token) — raw token never stored
339
+ token_prefix TEXT NOT NULL -- first 8 chars, safe for display
340
+ is_active BOOLEAN DEFAULT true
341
+ created_at / rotated_at TIMESTAMPTZ
342
+ ```
343
+
344
+ **Claim atomicity** is enforced at the database level:
345
+
346
+ ```sql
347
+ UPDATE print_jobs
348
+ SET status = 'processing', claimed_by = $agents_id, claimed_at = NOW()
349
+ WHERE id = $id AND status = 'queued' AND claimed_by IS NULL
350
+ RETURNING *
351
+ -- zero rows → route returns 409
352
+ ```
353
+
354
+ ---
355
+
356
+ ## Open / Deferred
357
+
358
+ | Item | Status |
359
+ |------|--------|
360
+ | USB transport (Windows Zadig driver setup) | v1.1 |
361
+ | `savepoint-bridge status` implementation | shipped |
362
+ | `savepoint-bridge logs` tail implementation | shipped |
363
+ | Self-update apply | v1.1 (currently: startup notice only) |
364
+ | Format transcoding (ZPL ↔ ESC/P) | v2 |
365
+ | Token expiry reminders (90-day rotation) | v1.1 |
366
+ | Printer-aware job claiming (skip offline printers) | Deferred |
@@ -0,0 +1,56 @@
1
+ [
2
+ {
3
+ "model": "Zebra ZT230",
4
+ "manufacturer": "Zebra",
5
+ "expected_format": "zpl",
6
+ "default_port": 9100,
7
+ "connection_types": ["network", "usb"],
8
+ "vid": 2655,
9
+ "pid": 516
10
+ },
11
+ {
12
+ "model": "Zebra ZT410",
13
+ "manufacturer": "Zebra",
14
+ "expected_format": "zpl",
15
+ "default_port": 9100,
16
+ "connection_types": ["network", "usb"],
17
+ "vid": 2655,
18
+ "pid": 519
19
+ },
20
+ {
21
+ "model": "Zebra GK420d",
22
+ "manufacturer": "Zebra",
23
+ "expected_format": "zpl",
24
+ "default_port": 9100,
25
+ "connection_types": ["network", "usb"],
26
+ "vid": 2655,
27
+ "pid": 515
28
+ },
29
+ {
30
+ "model": "Epson TM-T88",
31
+ "manufacturer": "Epson",
32
+ "expected_format": "esc-p",
33
+ "default_port": 9100,
34
+ "connection_types": ["network", "usb"],
35
+ "vid": 1208,
36
+ "pid": 514
37
+ },
38
+ {
39
+ "model": "Epson TM-T20",
40
+ "manufacturer": "Epson",
41
+ "expected_format": "esc-p",
42
+ "default_port": 9100,
43
+ "connection_types": ["network", "usb"],
44
+ "vid": 1208,
45
+ "pid": 1024
46
+ },
47
+ {
48
+ "model": "Brother QL-820NWB",
49
+ "manufacturer": "Brother",
50
+ "expected_format": "raw",
51
+ "default_port": 9100,
52
+ "connection_types": ["network", "usb"],
53
+ "vid": 1273,
54
+ "pid": 8201
55
+ }
56
+ ]
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};