synqdb-agent 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.
Files changed (3) hide show
  1. package/README.md +129 -0
  2. package/index.js +237 -0
  3. package/package.json +24 -0
package/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # synqdb-agent
2
+
3
+ Local database relay agent for [SynqDB](https://synqdb.com) — connects your local databases to the SynqDB cloud dashboard without any firewall changes or port forwarding.
4
+
5
+ ## How it works
6
+
7
+ The agent runs on your machine and opens an outbound WebSocket connection to the SynqDB API. When you query a local cluster from the dashboard, the API routes the query through this connection, the agent executes it against your local database, and returns the result.
8
+
9
+ ```
10
+ SynqDB Dashboard → API → Agent (your machine) → Local Database
11
+ ```
12
+
13
+ ## Requirements
14
+
15
+ - Node.js 18 or higher
16
+ - A local MySQL, PostgreSQL, or SQL Server database
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install -g synqdb-agent
22
+ ```
23
+
24
+ Or run without installing:
25
+
26
+ ```bash
27
+ npx synqdb-agent <agentKey>
28
+ ```
29
+
30
+ ## Getting your agent key
31
+
32
+ 1. Open the [SynqDB dashboard](https://synqdb.com)
33
+ 2. Click **Add Connection**
34
+ 3. Toggle **Local Database**
35
+ 4. Fill in your local DB credentials and click **Generate Agent Key**
36
+ 5. Copy the key — it is only shown once
37
+
38
+ ## Usage
39
+
40
+ ### Recommended — save once, run forever
41
+
42
+ On first use, save your key:
43
+
44
+ ```bash
45
+ synqdb-agent --save <agentKey>
46
+ ```
47
+
48
+ From then on, just run:
49
+
50
+ ```bash
51
+ synqdb-agent
52
+ ```
53
+
54
+ The key is stored in `~/.synqdb-agent` (readable only by your user account).
55
+
56
+ ### Pass the key each time
57
+
58
+ ```bash
59
+ synqdb-agent <agentKey>
60
+ ```
61
+
62
+ The first successful connection will also auto-save the key so future runs need no arguments.
63
+
64
+ ### Using environment variables
65
+
66
+ ```bash
67
+ SYNQDB_AGENT_KEY=abc-123-def-456 synqdb-agent
68
+ ```
69
+
70
+ Or place them in a `.env` file in the directory where you run the agent:
71
+
72
+ ```env
73
+ SYNQDB_AGENT_KEY=abc-123-def-456
74
+ SYNQDB_SERVER_URL=https://api.synqdb.com
75
+ ```
76
+
77
+ ### Key resolution order
78
+
79
+ The agent looks for the key in this order:
80
+
81
+ 1. CLI argument (`synqdb-agent <key>`)
82
+ 2. `SYNQDB_AGENT_KEY` environment variable
83
+ 3. Saved config at `~/.synqdb-agent`
84
+
85
+ ### Environment variables
86
+
87
+ | Variable | Description | Default |
88
+ |---|---|---|
89
+ | `SYNQDB_AGENT_KEY` | Your agent key | — |
90
+ | `SYNQDB_SERVER_URL` | SynqDB API URL | `https://api.synqdb.com` |
91
+
92
+ ## Running persistently
93
+
94
+ To keep the agent running in the background across terminal sessions and machine restarts, use [PM2](https://pm2.keymetrics.io):
95
+
96
+ ```bash
97
+ npm install -g pm2
98
+ pm2 start synqdb-agent --name synqdb-agent -- <agentKey>
99
+ pm2 save
100
+ pm2 startup
101
+ ```
102
+
103
+ Useful PM2 commands:
104
+
105
+ ```bash
106
+ pm2 logs synqdb-agent # view live logs
107
+ pm2 status # check running status
108
+ pm2 restart synqdb-agent
109
+ pm2 stop synqdb-agent
110
+ ```
111
+
112
+ ## Supported databases
113
+
114
+ | Database | Driver |
115
+ |---|---|
116
+ | MySQL / MariaDB | `mysql2` |
117
+ | PostgreSQL | `pg` |
118
+ | SQL Server (MSSQL) | `mssql` |
119
+
120
+ ## Security
121
+
122
+ - Your agent key is a 128-bit random UUID — treat it like a password
123
+ - The agent connects **outbound only** — no inbound ports are opened on your machine
124
+ - All queries are constructed server-side; the agent never builds SQL from user input
125
+ - Credentials stay on your machine and are never sent to the SynqDB API
126
+
127
+ ## License
128
+
129
+ MIT
package/index.js ADDED
@@ -0,0 +1,237 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // Load .env from the directory where the command is run, if it exists
5
+ try { require('dotenv').config(); } catch {}
6
+
7
+ const { io } = require('socket.io-client');
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const os = require('os');
11
+
12
+ const CONFIG_PATH = path.join(os.homedir(), '.synqdb-agent');
13
+
14
+ function loadConfig() {
15
+ try {
16
+ return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function saveConfig(data) {
23
+ const existing = loadConfig();
24
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify({ ...existing, ...data }, null, 2), {
25
+ mode: 0o600, // owner read/write only
26
+ });
27
+ }
28
+
29
+ // ─── Resolve agentKey and serverUrl ──────────────────────────────────────────
30
+
31
+ const saved = loadConfig();
32
+
33
+ // --save flag: persist key (and optional server URL) then exit
34
+ if (process.argv.includes('--save')) {
35
+ const keyArg = process.argv.find((a) => !a.startsWith('-') && process.argv.indexOf(a) > 1);
36
+ const urlArg = process.argv[process.argv.indexOf('--save') + 1];
37
+ const keyToSave = keyArg || process.env.SYNQDB_AGENT_KEY;
38
+ const urlToSave = (!urlArg?.startsWith('-') ? urlArg : undefined) || process.env.SYNQDB_SERVER_URL;
39
+
40
+ if (!keyToSave) {
41
+ console.error('Usage: synqdb-agent --save <agentKey> [serverUrl]');
42
+ process.exit(1);
43
+ }
44
+
45
+ saveConfig({
46
+ agentKey: keyToSave,
47
+ ...(urlToSave ? { serverUrl: urlToSave } : {}),
48
+ });
49
+
50
+ console.log(`Saved to ${CONFIG_PATH}`);
51
+ console.log(` agentKey: ${keyToSave}`);
52
+ if (urlToSave) console.log(` serverUrl: ${urlToSave}`);
53
+ console.log('');
54
+ console.log('Run `synqdb-agent` with no arguments to start.');
55
+ process.exit(0);
56
+ }
57
+
58
+ const agentKey =
59
+ process.argv[2] ||
60
+ process.env.SYNQDB_AGENT_KEY ||
61
+ saved.agentKey;
62
+
63
+ const serverUrl =
64
+ process.argv[3] ||
65
+ process.env.SYNQDB_SERVER_URL ||
66
+ saved.serverUrl ||
67
+ 'https://api.synqdb.com';
68
+
69
+ if (!agentKey) {
70
+ console.error('');
71
+ console.error(' No agent key found. Options:');
72
+ console.error('');
73
+ console.error(' 1. Save key once (recommended):');
74
+ console.error(' synqdb-agent --save <agentKey>');
75
+ console.error(' synqdb-agent');
76
+ console.error('');
77
+ console.error(' 2. Pass key each time:');
78
+ console.error(' synqdb-agent <agentKey>');
79
+ console.error('');
80
+ console.error(' 3. Set environment variable:');
81
+ console.error(' SYNQDB_AGENT_KEY=<agentKey> synqdb-agent');
82
+ console.error('');
83
+ process.exit(1);
84
+ }
85
+
86
+ // If key came from args (not saved), prompt user to save it
87
+ if (process.argv[2] && !saved.agentKey) {
88
+ console.log(`Tip: run \`synqdb-agent --save ${process.argv[2]}\` to avoid typing the key next time.`);
89
+ }
90
+
91
+ console.log(`Connecting to SynqDB at ${serverUrl} ...`);
92
+
93
+ // Cache connections by a composite key so we don't open a new connection per query
94
+ const connectionCache = new Map();
95
+
96
+ function cacheKey(payload) {
97
+ return `${payload.type}:${payload.host}:${payload.port}:${payload.database}:${payload.username}`;
98
+ }
99
+
100
+ // ─── MySQL ────────────────────────────────────────────────────────────────────
101
+
102
+ async function runMySQL(payload) {
103
+ const mysql = require('mysql2/promise');
104
+ const key = cacheKey(payload);
105
+ let pool = connectionCache.get(key);
106
+ if (!pool) {
107
+ pool = mysql.createPool({
108
+ host: payload.host,
109
+ port: payload.port,
110
+ user: payload.username,
111
+ password: payload.password || undefined,
112
+ database: payload.database,
113
+ waitForConnections: true,
114
+ connectionLimit: 5,
115
+ multipleStatements: true,
116
+ });
117
+ connectionCache.set(key, pool);
118
+ }
119
+ const [rows] = await pool.query(payload.sql, payload.params || []);
120
+ const data = Array.isArray(rows) ? rows : [rows];
121
+ return { rows: data, rowCount: data.length };
122
+ }
123
+
124
+ // ─── PostgreSQL ───────────────────────────────────────────────────────────────
125
+
126
+ async function runPostgres(payload) {
127
+ const { Pool } = require('pg');
128
+ const key = cacheKey(payload);
129
+ let pool = connectionCache.get(key);
130
+ if (!pool) {
131
+ pool = new Pool({
132
+ host: payload.host,
133
+ port: payload.port,
134
+ user: payload.username,
135
+ password: payload.password || undefined,
136
+ database: payload.database,
137
+ max: 5,
138
+ idleTimeoutMillis: 30000,
139
+ connectionTimeoutMillis: 10000,
140
+ });
141
+ connectionCache.set(key, pool);
142
+ }
143
+ const res = await pool.query(payload.sql, payload.params || []);
144
+ return { rows: res.rows, rowCount: res.rowCount ?? res.rows.length };
145
+ }
146
+
147
+ // ─── MSSQL ────────────────────────────────────────────────────────────────────
148
+
149
+ async function runMSSQL(payload) {
150
+ const mssql = require('mssql');
151
+ const key = cacheKey(payload);
152
+ let pool = connectionCache.get(key);
153
+ if (!pool || !pool.connected) {
154
+ pool = new mssql.ConnectionPool({
155
+ server: payload.host,
156
+ port: payload.port || 1433,
157
+ user: payload.username,
158
+ password: payload.password || undefined,
159
+ database: payload.database,
160
+ options: { encrypt: true, trustServerCertificate: true },
161
+ });
162
+ await pool.connect();
163
+ connectionCache.set(key, pool);
164
+ }
165
+ const request = pool.request();
166
+ if (payload.namedParams) {
167
+ for (const [name, value] of Object.entries(payload.namedParams)) {
168
+ request.input(name, value);
169
+ }
170
+ }
171
+ const result = await request.query(payload.sql);
172
+ const rows = result.recordset || [];
173
+ return { rows, rowCount: result.rowsAffected?.[0] ?? rows.length };
174
+ }
175
+
176
+ // ─── Dispatch ─────────────────────────────────────────────────────────────────
177
+
178
+ async function executeQuery(payload) {
179
+ switch (payload.type) {
180
+ case 'mysql': return runMySQL(payload);
181
+ case 'postgres': return runPostgres(payload);
182
+ case 'mssql': return runMSSQL(payload);
183
+ default: throw new Error(`Unsupported database type: ${payload.type}`);
184
+ }
185
+ }
186
+
187
+ // ─── Socket.IO ────────────────────────────────────────────────────────────────
188
+
189
+ const socket = io(`${serverUrl}/agent`, {
190
+ reconnectionDelay: 2000,
191
+ reconnectionDelayMax: 10000,
192
+ transports: ['websocket'],
193
+ });
194
+
195
+ socket.on('connect', () => {
196
+ console.log('Connected. Authenticating ...');
197
+ socket.emit('register', { agentKey });
198
+ });
199
+
200
+ socket.on('registered', ({ clusterId }) => {
201
+ console.log(`Authenticated. Serving cluster: ${clusterId}`);
202
+ // If this was the first run with a key arg, auto-save it for next time
203
+ if (process.argv[2] && !saved.agentKey) {
204
+ saveConfig({ agentKey, serverUrl });
205
+ console.log(`Key saved to ${CONFIG_PATH} — next time just run \`synqdb-agent\``);
206
+ }
207
+ });
208
+
209
+ socket.on('auth_error', ({ message }) => {
210
+ console.error(`Authentication failed: ${message}`);
211
+ process.exit(1);
212
+ });
213
+
214
+ socket.on('query', async (payload) => {
215
+ const { requestId } = payload;
216
+ try {
217
+ const { rows, rowCount } = await executeQuery(payload);
218
+ socket.emit('result', { requestId, rows, rowCount });
219
+ } catch (err) {
220
+ console.error(`Query error [${requestId}]:`, err.message);
221
+ socket.emit('error', { requestId, message: err.message });
222
+ }
223
+ });
224
+
225
+ socket.on('disconnect', (reason) => {
226
+ console.log(`Disconnected: ${reason}. Reconnecting ...`);
227
+ });
228
+
229
+ socket.on('connect_error', (err) => {
230
+ console.error('Connection error:', err.message);
231
+ });
232
+
233
+ process.on('SIGINT', () => {
234
+ console.log('\nShutting down agent.');
235
+ socket.disconnect();
236
+ process.exit(0);
237
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "synqdb-agent",
3
+ "version": "1.0.0",
4
+ "description": "Local database relay agent for SynqDB — connects your local databases to the SynqDB cloud dashboard",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "synqdb-agent": "./index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node index.js"
11
+ },
12
+ "keywords": ["synqdb", "database", "agent", "relay", "mysql", "postgres", "mssql"],
13
+ "dependencies": {
14
+ "dotenv": "^16.0.0",
15
+ "mysql2": "^3.6.0",
16
+ "mssql": "^10.0.0",
17
+ "pg": "^8.11.0",
18
+ "socket.io-client": "^4.7.0"
19
+ },
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "license": "MIT"
24
+ }