@savepoint/bridge 1.0.0 → 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 +1 -1
- package/dist/daemon.js +2 -2
- package/dist/discovery.js +83 -6
- package/dist/logs.js +1 -1
- package/dist/service.js +11 -1
- package/dist/setup.js +22 -6
- package/dist/token.js +5 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
package/dist/daemon.js
CHANGED
|
@@ -7,8 +7,8 @@ import { discoverPrinters } from './discovery.js';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { DATA_PATH } from './logger.js';
|
|
10
|
-
const BASE_URL = process.env.SAVEPOINT_API_URL ?? 'https://
|
|
11
|
-
const VERSION = '1.0.
|
|
10
|
+
const BASE_URL = process.env.SAVEPOINT_APP_URL ?? process.env.SAVEPOINT_API_URL ?? 'https://app.savepointhq.com';
|
|
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
|
-
|
|
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(
|
|
31
|
-
|
|
32
|
-
|
|
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 [
|
|
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
|
|
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
|
@@ -4,8 +4,8 @@ import { saveToken } from './token.js';
|
|
|
4
4
|
import { discoverPrinters } from './discovery.js';
|
|
5
5
|
import { installService } from './service.js';
|
|
6
6
|
import { log } from './logger.js';
|
|
7
|
-
const BASE_URL = process.env.SAVEPOINT_API_URL ?? 'https://
|
|
8
|
-
const VERSION = '1.0.
|
|
7
|
+
const BASE_URL = process.env.SAVEPOINT_APP_URL ?? process.env.SAVEPOINT_API_URL ?? 'https://app.savepointhq.com';
|
|
8
|
+
const VERSION = '1.0.4';
|
|
9
9
|
function parseFlags(args) {
|
|
10
10
|
const flags = {};
|
|
11
11
|
for (const arg of args) {
|
|
@@ -21,6 +21,7 @@ export async function runSetup(args) {
|
|
|
21
21
|
const nonInteractive = flags['non-interactive'] === true;
|
|
22
22
|
const noService = flags['no-service'] === true;
|
|
23
23
|
const tokenFlag = typeof flags['token'] === 'string' ? flags['token'] : null;
|
|
24
|
+
const isWindows = process.platform === 'win32';
|
|
24
25
|
console.log();
|
|
25
26
|
console.log(pc.bold('╔══════════════════════════════════════════════════╗'));
|
|
26
27
|
console.log(pc.bold('║ SavePoint Bridge v' + VERSION.padEnd(22) + '║'));
|
|
@@ -40,6 +41,13 @@ What it stores on this computer:
|
|
|
40
41
|
|
|
41
42
|
You can revoke access at any time from:
|
|
42
43
|
SavePoint > Settings > Bridge > Revoke
|
|
44
|
+
|
|
45
|
+
${isWindows ? `Windows setup notes:
|
|
46
|
+
- Open Windows PowerShell as Administrator
|
|
47
|
+
- Do not use Command Prompt
|
|
48
|
+
- If PowerShell blocks scripts, run:
|
|
49
|
+
Set-ExecutionPolicy -Scope Process Bypass -Force
|
|
50
|
+
` : ''}
|
|
43
51
|
`);
|
|
44
52
|
if (!nonInteractive) {
|
|
45
53
|
const proceed = await p.confirm({ message: 'Continue with setup?' });
|
|
@@ -56,7 +64,9 @@ You can revoke access at any time from:
|
|
|
56
64
|
}
|
|
57
65
|
else {
|
|
58
66
|
const input = await p.text({
|
|
59
|
-
message:
|
|
67
|
+
message: isWindows
|
|
68
|
+
? 'Open SavePoint > Settings > Bridge > Add Bridge, then paste your token here (PowerShell only):'
|
|
69
|
+
: 'Open SavePoint > Settings > Bridge > Add Bridge and paste your token:',
|
|
60
70
|
placeholder: 'Paste token here...',
|
|
61
71
|
validate: v => v.length < 32 ? 'Token looks too short -- check you copied the full value' : undefined,
|
|
62
72
|
});
|
|
@@ -79,8 +89,14 @@ You can revoke access at any time from:
|
|
|
79
89
|
body: JSON.stringify({ version: VERSION }),
|
|
80
90
|
});
|
|
81
91
|
if (!res.ok) {
|
|
82
|
-
|
|
83
|
-
|
|
92
|
+
const details = await res.text().catch(() => '');
|
|
93
|
+
const suffix = res.status === 401
|
|
94
|
+
? 'The token was not recognized. Generate a fresh token in SavePoint > Settings > Bridge > Add Bridge and copy the full value.'
|
|
95
|
+
: details
|
|
96
|
+
? details.trim()
|
|
97
|
+
: `HTTP ${res.status}`;
|
|
98
|
+
spinner.stop(pc.red(`Token verification failed -- ${suffix}`));
|
|
99
|
+
throw new Error(suffix);
|
|
84
100
|
}
|
|
85
101
|
spinner.stop(pc.green('Token verified'));
|
|
86
102
|
}
|
|
@@ -90,7 +106,7 @@ You can revoke access at any time from:
|
|
|
90
106
|
console.log(pc.yellow('\nSSL error -- if your network uses an SSL proxy, set:'));
|
|
91
107
|
console.log(pc.yellow(' NODE_EXTRA_CA_CERTS=/path/to/proxy-ca.crt savepoint-bridge setup\n'));
|
|
92
108
|
}
|
|
93
|
-
|
|
109
|
+
throw e instanceof Error ? e : new Error('Token verification failed');
|
|
94
110
|
}
|
|
95
111
|
await saveToken(token);
|
|
96
112
|
p.intro(pc.bold('Step 2 of 3 -- Printers'));
|
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
|
-
|
|
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);
|