synqdb-agent 1.0.3 → 1.0.5

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 +39 -40
  2. package/index.js +226 -175
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # synqdb-agent
2
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.
3
+ Local database relay agent for [SynqDB](https://synqdb.live) — connects your local databases to the SynqDB cloud dashboard without any firewall changes or port forwarding.
4
4
 
5
5
  ## How it works
6
6
 
@@ -24,78 +24,72 @@ npm install -g synqdb-agent
24
24
  Or run without installing:
25
25
 
26
26
  ```bash
27
- npx synqdb-agent <agentKey>
27
+ npx synqdb-agent login
28
+ npx synqdb-agent
28
29
  ```
29
30
 
30
- ## Getting your agent key
31
+ ## Authentication
31
32
 
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:
33
+ Authentication is browser-based no keys to copy or paste.
43
34
 
44
35
  ```bash
45
- synqdb-agent --save <agentKey>
36
+ synqdb-agent login
46
37
  ```
47
38
 
48
- From then on, just run:
39
+ This opens your browser to the SynqDB app. Log in (if you aren't already) and click **Authorize**. The CLI detects approval automatically and saves the credential to `~/.synqdb-agent`.
40
+
41
+ After that, just run:
49
42
 
50
43
  ```bash
51
44
  synqdb-agent
52
45
  ```
53
46
 
54
- The key is stored in `~/.synqdb-agent` (readable only by your user account).
47
+ ## Usage
55
48
 
56
- ### Pass the key each time
49
+ ### Step 1 Log in
57
50
 
58
51
  ```bash
59
- synqdb-agent <agentKey>
52
+ synqdb-agent login
60
53
  ```
61
54
 
62
- The first successful connection will also auto-save the key so future runs need no arguments.
55
+ Opens your browser click **Authorize** done. Credential is saved automatically.
63
56
 
64
- ### Using environment variables
57
+ ### Step 2 — Start the agent
65
58
 
66
59
  ```bash
67
- SYNQDB_AGENT_KEY=abc-123-def-456 synqdb-agent
60
+ synqdb-agent
68
61
  ```
69
62
 
70
- Or place them in a `.env` file in the directory where you run the agent:
63
+ The agent connects and stays running. Queries from your dashboard are routed through it in real time.
64
+
65
+ ### Environment variable override
66
+
67
+ If you need to supply the key directly (e.g. in a CI/CD pipeline or Docker container), set:
71
68
 
72
69
  ```env
73
- SYNQDB_AGENT_KEY=abc-123-def-456
74
- SYNQDB_SERVER_URL=https://api.synqdb.com
70
+ SYNQDB_AGENT_KEY=<your-agent-key>
75
71
  ```
76
72
 
77
- ### Key resolution order
73
+ The key resolution order is:
74
+ 1. `SYNQDB_AGENT_KEY` environment variable
75
+ 2. Saved credential at `~/.synqdb-agent` (written by `synqdb-agent login`)
78
76
 
79
- The agent looks for the key in this order:
77
+ ### Custom server URL
80
78
 
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
79
+ ```env
80
+ SYNQDB_SERVER_URL=https://api.synqdb.live
81
+ SYNQDB_FRONTEND_URL=https://synqdb.live
82
+ ```
86
83
 
87
- | Variable | Description | Default |
88
- |---|---|---|
89
- | `SYNQDB_AGENT_KEY` | Your agent key | — |
90
- | `SYNQDB_SERVER_URL` | SynqDB API URL | `https://api.synqdb.com` |
84
+ Or place them in a `.env` file in the directory where you run the agent.
91
85
 
92
86
  ## Running persistently
93
87
 
94
- To keep the agent running in the background across terminal sessions and machine restarts, use [PM2](https://pm2.keymetrics.io):
88
+ To keep the agent running across terminal sessions and machine restarts, use [PM2](https://pm2.keymetrics.io):
95
89
 
96
90
  ```bash
97
91
  npm install -g pm2
98
- pm2 start synqdb-agent --name synqdb-agent -- <agentKey>
92
+ pm2 start synqdb-agent --name synqdb-agent
99
93
  pm2 save
100
94
  pm2 startup
101
95
  ```
@@ -109,6 +103,10 @@ pm2 restart synqdb-agent
109
103
  pm2 stop synqdb-agent
110
104
  ```
111
105
 
106
+ ## Revoking access
107
+
108
+ If you need to invalidate the current credential (e.g. the machine was compromised), go to **Dashboard → Project Settings → Local Agent → Rotate Key**. Any running agent will be disconnected. Run `synqdb-agent login` to re-authenticate.
109
+
112
110
  ## Supported databases
113
111
 
114
112
  | Database | Driver |
@@ -119,10 +117,11 @@ pm2 stop synqdb-agent
119
117
 
120
118
  ## Security
121
119
 
122
- - Your agent key is a 128-bit random UUID treat it like a password
120
+ - Authentication uses short-lived browser tokens (5-minute TTL)no long-lived secrets are ever transmitted in a URL or terminal output
121
+ - The saved credential in `~/.synqdb-agent` is readable only by your user account (mode `0600`)
123
122
  - The agent connects **outbound only** — no inbound ports are opened on your machine
124
123
  - 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
124
+ - Database credentials stay on your machine and are never sent to the SynqDB API
126
125
 
127
126
  ## License
128
127
 
package/index.js CHANGED
@@ -26,212 +26,263 @@ function saveConfig(data) {
26
26
  });
27
27
  }
28
28
 
29
- // ─── Resolve agentKey and serverUrl ──────────────────────────────────────────
29
+ // ─── Resolve serverUrl ────────────────────────────────────────────────────────
30
30
 
31
31
  const saved = loadConfig();
32
32
 
33
- // --save flag: persist key (and optional server URL) then exit
34
- if (process.argv.includes('--save')) {
35
- const saveIdx = process.argv.indexOf('--save');
36
- const nonFlagArgs = process.argv.slice(saveIdx + 1).filter((a) => !a.startsWith('-'));
37
- const keyToSave = nonFlagArgs[0] || process.env.SYNQDB_AGENT_KEY;
38
- const urlToSave = nonFlagArgs[1] || process.env.SYNQDB_SERVER_URL;
33
+ const serverUrl =
34
+ process.env.SYNQDB_SERVER_URL ||
35
+ (saved.serverUrl?.startsWith('http') ? saved.serverUrl : null) ||
36
+ 'https://api.synqdb.live';
37
+
38
+ const frontendUrl =
39
+ process.env.SYNQDB_FRONTEND_URL ||
40
+ (saved.frontendUrl?.startsWith('http') ? saved.frontendUrl : null) ||
41
+ 'https://synqdb.live';
39
42
 
40
- if (!keyToSave) {
41
- console.error('Usage: synqdb-agent --save <agentKey> [serverUrl]');
43
+ // ─── login command ────────────────────────────────────────────────────────────
44
+
45
+ if (process.argv[2] === 'login') {
46
+ runLogin().catch((err) => {
47
+ console.error('Login failed:', err.message);
42
48
  process.exit(1);
43
- }
49
+ });
50
+ } else {
51
+ runAgent();
52
+ }
44
53
 
45
- saveConfig({
46
- agentKey: keyToSave,
47
- serverUrl: urlToSave || null,
54
+ async function runLogin() {
55
+ // 1. Create a short-lived login token from the server
56
+ const initRes = await fetch(`${serverUrl}/v1/auth/cli-login/init`, {
57
+ method: 'POST',
58
+ headers: { 'Content-Type': 'application/json' },
48
59
  });
60
+ if (!initRes.ok) {
61
+ throw new Error(`Server error: ${initRes.status}`);
62
+ }
63
+ const { loginToken } = await initRes.json();
49
64
 
50
- console.log(`Saved to ${CONFIG_PATH}`);
51
- console.log(` agentKey: ${keyToSave}`);
52
- if (urlToSave) console.log(` serverUrl: ${urlToSave}`);
65
+ // 2. Open the authorize page in the user's browser
66
+ const authorizeUrl = `${frontendUrl}/agent/authorize?token=${loginToken}`;
67
+ console.log('');
68
+ console.log(' Opening browser for authentication...');
69
+ console.log(` If the browser does not open, visit:\n ${authorizeUrl}`);
53
70
  console.log('');
54
- console.log('Run `synqdb-agent` with no arguments to start.');
55
- process.exit(0);
56
- }
57
71
 
58
- const agentKey =
59
- process.argv[2] ||
60
- process.env.SYNQDB_AGENT_KEY ||
61
- saved.agentKey;
72
+ try {
73
+ const open = require('open');
74
+ await open(authorizeUrl);
75
+ } catch {
76
+ // open might not be available in all environments — URL is printed above
77
+ }
62
78
 
63
- const serverUrl =
64
- process.argv[3] ||
65
- process.env.SYNQDB_SERVER_URL ||
66
- (saved.serverUrl?.startsWith('http') ? saved.serverUrl : null) ||
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
- }
79
+ // 3. Poll the server until the user approves (or token expires ~5 min)
80
+ const POLL_INTERVAL_MS = 2000;
81
+ const POLL_TIMEOUT_MS = 5 * 60 * 1000;
82
+ const deadline = Date.now() + POLL_TIMEOUT_MS;
85
83
 
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
- }
84
+ process.stdout.write(' Waiting for approval');
90
85
 
91
- console.log(`Connecting to SynqDB at ${serverUrl} ...`);
86
+ while (Date.now() < deadline) {
87
+ await sleep(POLL_INTERVAL_MS);
88
+ process.stdout.write('.');
92
89
 
93
- // Cache connections by a composite key so we don't open a new connection per query
94
- const connectionCache = new Map();
90
+ let pollRes;
91
+ try {
92
+ pollRes = await fetch(`${serverUrl}/v1/auth/cli-login/poll/${loginToken}`);
93
+ } catch {
94
+ // transient network error — keep retrying
95
+ continue;
96
+ }
95
97
 
96
- function cacheKey(payload) {
97
- return `${payload.type}:${payload.host}:${payload.port}:${payload.database}:${payload.username}`;
98
- }
98
+ if (!pollRes.ok) {
99
+ process.stdout.write('\n');
100
+ throw new Error(`Token expired or invalid (server returned ${pollRes.status})`);
101
+ }
102
+
103
+ const body = await pollRes.json();
99
104
 
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);
105
+ if (body.status === 'authorized') {
106
+ process.stdout.write('\n');
107
+ saveConfig({ agentKey: body.agentKey, serverUrl });
108
+ console.log('');
109
+ console.log(' Authenticated! Agent key saved to', CONFIG_PATH);
110
+ console.log(' Run `synqdb-agent` with no arguments to start the agent.');
111
+ console.log('');
112
+ return;
113
+ }
118
114
  }
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 };
115
+
116
+ process.stdout.write('\n');
117
+ throw new Error('Login timed out. Please run `synqdb-agent login` again.');
122
118
  }
123
119
 
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 };
120
+ function sleep(ms) {
121
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
122
  }
146
123
 
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);
124
+ // ─── Agent runner ─────────────────────────────────────────────────────────────
125
+
126
+ function runAgent() {
127
+ const agentKey = process.env.SYNQDB_AGENT_KEY || saved.agentKey;
128
+
129
+ if (!agentKey) {
130
+ console.error('');
131
+ console.error(' No agent key found. Run:');
132
+ console.error('');
133
+ console.error(' synqdb-agent login');
134
+ console.error('');
135
+ console.error(' This opens your browser to authenticate — no key to copy.');
136
+ console.error('');
137
+ process.exit(1);
164
138
  }
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
- }
139
+
140
+ console.log(`Connecting to SynqDB at ${serverUrl} ...`);
141
+
142
+ // Cache connections by a composite key so we don't open a new connection per query
143
+ const connectionCache = new Map();
144
+
145
+ function cacheKey(payload) {
146
+ return `${payload.type}:${payload.host}:${payload.port}:${payload.database}:${payload.username}`;
170
147
  }
171
- const result = await request.query(payload.sql);
172
- const rows = result.recordset || [];
173
- return { rows, rowCount: result.rowsAffected?.[0] ?? rows.length };
174
- }
175
148
 
176
- // ─── Dispatch ─────────────────────────────────────────────────────────────────
149
+ // ─── MySQL ──────────────────────────────────────────────────────────────────
150
+
151
+ async function runMySQL(payload) {
152
+ const mysql = require('mysql2/promise');
153
+ const key = cacheKey(payload);
154
+ let pool = connectionCache.get(key);
155
+ if (!pool) {
156
+ pool = mysql.createPool({
157
+ host: payload.host,
158
+ port: payload.port,
159
+ user: payload.username,
160
+ password: payload.password || undefined,
161
+ database: payload.database,
162
+ waitForConnections: true,
163
+ connectionLimit: 5,
164
+ multipleStatements: true,
165
+ });
166
+ connectionCache.set(key, pool);
167
+ }
168
+ const [rows] = await pool.query(payload.sql, payload.params || []);
169
+ const data = Array.isArray(rows) ? rows : [rows];
170
+ return { rows: data, rowCount: data.length };
171
+ }
177
172
 
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}`);
173
+ // ─── PostgreSQL ─────────────────────────────────────────────────────────────
174
+
175
+ async function runPostgres(payload) {
176
+ const { Pool } = require('pg');
177
+ const key = cacheKey(payload);
178
+ let pool = connectionCache.get(key);
179
+ if (!pool) {
180
+ pool = new Pool({
181
+ host: payload.host,
182
+ port: payload.port,
183
+ user: payload.username,
184
+ password: payload.password || undefined,
185
+ database: payload.database,
186
+ max: 5,
187
+ idleTimeoutMillis: 30000,
188
+ connectionTimeoutMillis: 10000,
189
+ });
190
+ connectionCache.set(key, pool);
191
+ }
192
+ const res = await pool.query(payload.sql, payload.params || []);
193
+ return { rows: res.rows, rowCount: res.rowCount ?? res.rows.length };
184
194
  }
185
- }
186
195
 
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\``);
196
+ // ─── MSSQL ──────────────────────────────────────────────────────────────────
197
+
198
+ async function runMSSQL(payload) {
199
+ const mssql = require('mssql');
200
+ const key = cacheKey(payload);
201
+ let pool = connectionCache.get(key);
202
+ if (!pool || !pool.connected) {
203
+ pool = new mssql.ConnectionPool({
204
+ server: payload.host,
205
+ port: payload.port || 1433,
206
+ user: payload.username,
207
+ password: payload.password || undefined,
208
+ database: payload.database,
209
+ options: { encrypt: true, trustServerCertificate: true },
210
+ });
211
+ await pool.connect();
212
+ connectionCache.set(key, pool);
213
+ }
214
+ const request = pool.request();
215
+ if (payload.namedParams) {
216
+ for (const [name, value] of Object.entries(payload.namedParams)) {
217
+ request.input(name, value);
218
+ }
219
+ }
220
+ const result = await request.query(payload.sql);
221
+ const rows = result.recordset || [];
222
+ return { rows, rowCount: result.rowsAffected?.[0] ?? rows.length };
206
223
  }
207
- });
208
224
 
209
- socket.on('auth_error', ({ message }) => {
210
- console.error(`Authentication failed: ${message}`);
211
- process.exit(1);
212
- });
225
+ // ─── Dispatch ────────────────────────────────────────────────────────────────
213
226
 
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 });
227
+ async function executeQuery(payload) {
228
+ switch (payload.type) {
229
+ case 'mysql': return runMySQL(payload);
230
+ case 'postgres': return runPostgres(payload);
231
+ case 'mssql': return runMSSQL(payload);
232
+ default: throw new Error(`Unsupported database type: ${payload.type}`);
233
+ }
222
234
  }
223
- });
224
235
 
225
- socket.on('disconnect', (reason) => {
226
- console.log(`Disconnected: ${reason}. Reconnecting ...`);
227
- });
236
+ // ─── Socket.IO ──────────────────────────────────────────────────────────────
237
+
238
+ const socket = io(`${serverUrl}/agent`, {
239
+ reconnectionDelay: 2000,
240
+ reconnectionDelayMax: 10000,
241
+ transports: ['websocket'],
242
+ });
243
+
244
+ socket.on('connect', () => {
245
+ console.log('Connected. Authenticating ...');
246
+ socket.emit('register', { agentKey });
247
+ });
248
+
249
+ socket.on('registered', ({ clusterId }) => {
250
+ console.log(`Authenticated. Serving cluster: ${clusterId}`);
251
+ });
228
252
 
229
- socket.on('connect_error', (err) => {
230
- console.error('Connection error:', err.message);
231
- });
253
+ socket.on('auth_error', ({ message }) => {
254
+ if (message && message.includes('rotated')) {
255
+ console.error(`\n ${message}`);
256
+ console.error(' Run `synqdb-agent login` to re-authenticate.\n');
257
+ } else {
258
+ console.error(`Authentication failed: ${message}`);
259
+ console.error('Run `synqdb-agent login` to re-authenticate.');
260
+ }
261
+ process.exit(1);
262
+ });
232
263
 
233
- process.on('SIGINT', () => {
234
- console.log('\nShutting down agent.');
235
- socket.disconnect();
236
- process.exit(0);
237
- });
264
+ socket.on('query', async (payload) => {
265
+ const { requestId } = payload;
266
+ try {
267
+ const { rows, rowCount } = await executeQuery(payload);
268
+ socket.emit('result', { requestId, rows, rowCount });
269
+ } catch (err) {
270
+ console.error(`Query error [${requestId}]:`, err.message);
271
+ socket.emit('error', { requestId, message: err.message });
272
+ }
273
+ });
274
+
275
+ socket.on('disconnect', (reason) => {
276
+ console.log(`Disconnected: ${reason}. Reconnecting ...`);
277
+ });
278
+
279
+ socket.on('connect_error', (err) => {
280
+ console.error('Connection error:', err.message);
281
+ });
282
+
283
+ process.on('SIGINT', () => {
284
+ console.log('\nShutting down agent.');
285
+ socket.disconnect();
286
+ process.exit(0);
287
+ });
288
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synqdb-agent",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "Local database relay agent for SynqDB — connects your local databases to the SynqDB cloud dashboard",
5
5
  "main": "index.js",
6
6
  "bin": {