@savepoint/bridge 1.0.2 → 1.0.4

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/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import pc from 'picocolors';
3
- const VERSION = '1.0.2';
3
+ const VERSION = '1.0.4';
4
4
  const [, , command, ...args] = process.argv;
5
5
  const COMMANDS = {
6
6
  setup: 'Interactive guided setup wizard',
package/dist/daemon.js CHANGED
@@ -8,7 +8,7 @@ import { join } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { DATA_PATH } from './logger.js';
10
10
  const BASE_URL = process.env.SAVEPOINT_APP_URL ?? process.env.SAVEPOINT_API_URL ?? 'https://app.savepointhq.com';
11
- const VERSION = '1.0.2';
11
+ const VERSION = '1.0.4';
12
12
  const DB_PATH = join(DATA_PATH, 'registry.db');
13
13
  const CAPS_PATH = fileURLToPath(new URL('../../capabilities/printers.json', import.meta.url));
14
14
  const REGISTRY_REFRESH_MS = 5 * 60 * 1000;
package/dist/discovery.js CHANGED
@@ -3,6 +3,8 @@ import { networkInterfaces } from 'os';
3
3
  import { execFile } from 'child_process';
4
4
  import { promisify } from 'util';
5
5
  const execFileAsync = promisify(execFile);
6
+ // Ports probed per host — ordered by likelihood for label/receipt printers
7
+ const PRINTER_PORTS = [9100, 631, 515];
6
8
  function probePort(host, port, timeoutMs = 1500) {
7
9
  return new Promise(resolve => {
8
10
  const socket = createConnection({ host, port });
@@ -23,15 +25,67 @@ function getSubnetHosts() {
23
25
  }
24
26
  return [];
25
27
  }
26
- async function scanNetwork(concurrency = 50) {
28
+ /** Probe a single host on all printer ports; returns first open port found or null */
29
+ async function probeHost(host) {
30
+ for (const port of PRINTER_PORTS) {
31
+ if (await probePort(host, port)) {
32
+ return { name: host, host, port, connection_type: 'network' };
33
+ }
34
+ }
35
+ return null;
36
+ }
37
+ async function scanNetwork(concurrency = 30) {
27
38
  const hosts = getSubnetHosts();
28
39
  const found = [];
29
40
  for (let i = 0; i < hosts.length; i += concurrency) {
30
- await Promise.allSettled(hosts.slice(i, i + concurrency).map(async (host) => {
31
- if (await probePort(host, 9100)) {
32
- found.push({ name: host, host, port: 9100, connection_type: 'network' });
41
+ const batch = await Promise.allSettled(hosts.slice(i, i + concurrency).map(host => probeHost(host)));
42
+ for (const result of batch) {
43
+ if (result.status === 'fulfilled' && result.value) {
44
+ found.push(result.value);
33
45
  }
34
- }));
46
+ }
47
+ }
48
+ return found;
49
+ }
50
+ /**
51
+ * mDNS/Bonjour discovery — resolves printer names and models on the LAN.
52
+ * Uses `dns-sd` on macOS and `avahi-browse` on Linux; no-ops silently on Windows.
53
+ */
54
+ async function scanMdns() {
55
+ const found = [];
56
+ if (process.platform === 'darwin') {
57
+ try {
58
+ // Browse for IPP and socket (raw) printer services for 5 seconds
59
+ const { stdout } = await execFileAsync('dns-sd', ['-B', '_ipp._tcp,_pdl-datastream._tcp', 'local.'], { timeout: 5000 }).catch(() => execFileAsync('dns-sd', ['-B', '_ipp._tcp', 'local.'], { timeout: 5000 }));
60
+ for (const line of stdout.split('\n')) {
61
+ // Lines look like: "Add 2 1 local. _ipp._tcp. My Printer"
62
+ const match = line.match(/Add\s+\d+\s+\d+\s+\S+\s+\S+\s+(.+)/);
63
+ if (match) {
64
+ const name = match[1].trim();
65
+ if (name)
66
+ found.push({ name, connection_type: 'network' });
67
+ }
68
+ }
69
+ }
70
+ catch { /* dns-sd timeout or unavailable */ }
71
+ }
72
+ else if (process.platform === 'linux') {
73
+ try {
74
+ const { stdout } = await execFileAsync('avahi-browse', ['-t', '-r', '-p', '_ipp._tcp'], { timeout: 5000 });
75
+ for (const line of stdout.split('\n')) {
76
+ // Resolved lines: "=;eth0;IPv4;My Printer;_ipp._tcp;local;host.local;192.168.1.x;631;..."
77
+ const parts = line.split(';');
78
+ if (parts[0] === '=' && parts.length >= 8) {
79
+ const name = parts[3];
80
+ const host = parts[7];
81
+ const port = parseInt(parts[8], 10) || 631;
82
+ if (name && host) {
83
+ found.push({ name, host, port, connection_type: 'network' });
84
+ }
85
+ }
86
+ }
87
+ }
88
+ catch { /* avahi-browse unavailable */ }
35
89
  }
36
90
  return found;
37
91
  }
@@ -48,7 +102,30 @@ async function scanCups() {
48
102
  return []; // lpstat unavailable (Windows or CUPS not installed)
49
103
  }
50
104
  }
105
+ /** Merge mDNS results into port-scan results — mDNS names win over raw IP labels */
106
+ function merge(portScan, mdns) {
107
+ const byHost = new Map();
108
+ for (const p of portScan) {
109
+ if (p.host)
110
+ byHost.set(p.host, p);
111
+ }
112
+ for (const m of mdns) {
113
+ if (m.host && byHost.has(m.host)) {
114
+ // Enrich the port-scan entry with the friendly name from mDNS
115
+ byHost.set(m.host, { ...byHost.get(m.host), name: m.name });
116
+ }
117
+ else if (m.host) {
118
+ byHost.set(m.host, m);
119
+ }
120
+ else {
121
+ // mDNS-only (no IP resolved yet) — include as-is
122
+ byHost.set(m.name, m);
123
+ }
124
+ }
125
+ return Array.from(byHost.values());
126
+ }
51
127
  export async function discoverPrinters() {
52
- const [network, cups] = await Promise.all([scanNetwork(), scanCups()]);
128
+ const [portScan, mdns, cups] = await Promise.all([scanNetwork(), scanMdns(), scanCups()]);
129
+ const network = merge(portScan, mdns);
53
130
  return [...network, ...cups];
54
131
  }
package/dist/logs.js CHANGED
@@ -6,7 +6,7 @@ function getRecentLogFiles(limit = 2) {
6
6
  if (!existsSync(logDir))
7
7
  return [];
8
8
  return readdirSync(logDir)
9
- .filter((file) => file.endsWith('.log'))
9
+ .filter((file) => /^\d{4}-\d{2}-\d{2}\.log$/.test(file))
10
10
  .sort()
11
11
  .slice(-limit)
12
12
  .map((file) => join(logDir, file));
package/dist/service.js CHANGED
@@ -2,7 +2,7 @@ import { writeFileSync, mkdirSync, unlinkSync, existsSync } from 'fs';
2
2
  import { execFile, execFileSync } from 'child_process';
3
3
  import { promisify } from 'util';
4
4
  import { homedir, userInfo } from 'os';
5
- import { join } from 'path';
5
+ import { join, dirname } from 'path';
6
6
  import { log } from './logger.js';
7
7
  const execFileAsync = promisify(execFile);
8
8
  function getBinaryPath() {
@@ -21,6 +21,11 @@ async function installMacOS() {
21
21
  const logDir = join(homedir(), '.savepoint-bridge', 'logs');
22
22
  mkdirSync(plistDir, { recursive: true });
23
23
  mkdirSync(logDir, { recursive: true });
24
+ const home = homedir();
25
+ // Capture the node binary directory at install time so launchd's minimal
26
+ // PATH doesn't break nvm / Homebrew / Volta / system installs
27
+ const nodeBinDir = dirname(process.execPath);
28
+ const launchdPath = `/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:${nodeBinDir}`;
24
29
  writeFileSync(plistPath, `<?xml version="1.0" encoding="UTF-8"?>
25
30
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
26
31
  <plist version="1.0">
@@ -28,6 +33,11 @@ async function installMacOS() {
28
33
  <key>Label</key><string>com.savepoint.bridge</string>
29
34
  <key>ProgramArguments</key>
30
35
  <array><string>${binary}</string><string>start</string></array>
36
+ <key>EnvironmentVariables</key>
37
+ <dict>
38
+ <key>HOME</key><string>${home}</string>
39
+ <key>PATH</key><string>${launchdPath}</string>
40
+ </dict>
31
41
  <key>RunAtLoad</key><true/>
32
42
  <key>KeepAlive</key><true/>
33
43
  <key>StandardOutPath</key><string>${logDir}/stdout.log</string>
package/dist/setup.js CHANGED
@@ -5,7 +5,7 @@ import { discoverPrinters } from './discovery.js';
5
5
  import { installService } from './service.js';
6
6
  import { log } from './logger.js';
7
7
  const BASE_URL = process.env.SAVEPOINT_APP_URL ?? process.env.SAVEPOINT_API_URL ?? 'https://app.savepointhq.com';
8
- const VERSION = '1.0.2';
8
+ const VERSION = '1.0.4';
9
9
  function parseFlags(args) {
10
10
  const flags = {};
11
11
  for (const arg of args) {
package/dist/token.js CHANGED
@@ -42,9 +42,12 @@ export async function loadToken() {
42
42
  const keytar = await trykeytar();
43
43
  if (keytar) {
44
44
  try {
45
- return await keytar.getPassword(SERVICE, ACCOUNT);
45
+ const keytarToken = await keytar.getPassword(SERVICE, ACCOUNT);
46
+ if (keytarToken)
47
+ return keytarToken;
48
+ // null means "not in keychain" — fall through to file fallback
46
49
  }
47
- catch { /* fall through */ }
50
+ catch { /* keychain inaccessible — fall through */ }
48
51
  }
49
52
  try {
50
53
  const raw = readFileSync(FALLBACK_PATH);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@savepoint/bridge",
3
- "version": "1.0.2",
3
+ "version": "1.0.4",
4
4
  "private": false,
5
5
  "description": "SavePoint Bridge hardware daemon",
6
6
  "keywords": [