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.
- package/README.md +129 -0
- package/index.js +237 -0
- 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
|
+
}
|