rds_ssm_connect 1.7.5 → 1.7.7

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.
@@ -28,10 +28,6 @@ jobs:
28
28
  rust_target: x86_64-unknown-linux-gnu
29
29
  sidecar_target: node22-linux-x64
30
30
  sidecar_triple: x86_64-unknown-linux-gnu
31
- - platform: ubuntu-24.04-arm64
32
- rust_target: aarch64-unknown-linux-gnu
33
- sidecar_target: node22-linux-arm64
34
- sidecar_triple: aarch64-unknown-linux-gnu
35
31
  - platform: windows-latest
36
32
  rust_target: x86_64-pc-windows-msvc
37
33
  sidecar_target: node22-win-x64
@@ -105,9 +101,16 @@ jobs:
105
101
  id: version
106
102
  run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
107
103
 
104
+ - name: Download signature files from release
105
+ env:
106
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
107
+ run: |
108
+ mkdir -p sigs
109
+ gh release download "v${{ steps.version.outputs.VERSION }}" --pattern '*.sig' --dir sigs || true
110
+
108
111
  - name: Generate latest.json
109
112
  run: |
110
- node scripts/generate-update-json.js ${{ steps.version.outputs.VERSION }} https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.VERSION }}
113
+ node scripts/generate-update-json.js ${{ steps.version.outputs.VERSION }} https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.VERSION }} sigs
111
114
 
112
115
  - name: Upload latest.json to release
113
116
  uses: softprops/action-gh-release@v1
package/README.md CHANGED
@@ -22,17 +22,85 @@ Secure database tunneling to AWS RDS through SSM port forwarding via bastion hos
22
22
 
23
23
  ## Installation
24
24
 
25
- ### Desktop App
25
+ ### macOS (Homebrew)
26
26
 
27
- Download the latest installer for your platform from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases):
27
+ ```bash
28
+ brew tap yarka-guru/tap
29
+ brew install --cask rds-ssm-connect
30
+ ```
28
31
 
29
- | Platform | Format |
30
- |----------|--------|
31
- | macOS (Apple Silicon + Intel) | `.dmg` |
32
- | Windows | `.msi` / `.exe` |
33
- | Linux | `.deb` / `.AppImage` |
32
+ This installs the desktop app along with `aws-vault` and `awscli` dependencies. You also need the [Session Manager Plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html):
34
33
 
35
- ### CLI
34
+ ```bash
35
+ brew install --cask session-manager-plugin
36
+ ```
37
+
38
+ Or download the `.dmg` directly from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases).
39
+
40
+ ### Linux
41
+
42
+ #### Option A: Homebrew
43
+
44
+ ```bash
45
+ # Install Homebrew if not already installed
46
+ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
47
+ echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> ~/.bashrc
48
+ eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
49
+
50
+ # Install the app (includes aws-vault and awscli as dependencies)
51
+ brew tap yarka-guru/tap
52
+ brew install yarka-guru/tap/rds-ssm-connect
53
+
54
+ # Install Session Manager Plugin (ARM64)
55
+ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_arm64/session-manager-plugin.deb" -o session-manager-plugin.deb
56
+ sudo dpkg -i session-manager-plugin.deb
57
+ # For x86_64, replace ubuntu_arm64 with ubuntu_64bit
58
+
59
+ # Make brew tools visible to the desktop app
60
+ echo 'export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"' | sudo tee /etc/profile.d/linuxbrew.sh
61
+ # Log out and back in for this to take effect
62
+ ```
63
+
64
+ #### Option B: Direct .deb install
65
+
66
+ ```bash
67
+ # 1. Download and install the app
68
+ # ARM64:
69
+ wget https://github.com/yarka-guru/connection_app/releases/latest/download/RDS.SSM.Connect_1.7.5_arm64.deb
70
+ sudo dpkg -i RDS.SSM.Connect_1.7.5_arm64.deb
71
+ # x86_64:
72
+ # wget https://github.com/yarka-guru/connection_app/releases/latest/download/RDS.SSM.Connect_1.7.5_amd64.deb
73
+ # sudo dpkg -i RDS.SSM.Connect_1.7.5_amd64.deb
74
+
75
+ # 2. Install aws-vault
76
+ # ARM64:
77
+ wget https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-arm64 -O aws-vault
78
+ # x86_64:
79
+ # wget https://github.com/99designs/aws-vault/releases/latest/download/aws-vault-linux-amd64 -O aws-vault
80
+ chmod +x aws-vault && sudo mv aws-vault /usr/local/bin/
81
+
82
+ # 3. Install AWS CLI
83
+ # ARM64:
84
+ curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o awscliv2.zip
85
+ # x86_64:
86
+ # curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip
87
+ unzip awscliv2.zip && sudo ./aws/install
88
+
89
+ # 4. Install Session Manager Plugin
90
+ # ARM64:
91
+ curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_arm64/session-manager-plugin.deb" -o session-manager-plugin.deb
92
+ # x86_64:
93
+ # curl "https://s3.amazonaws.com/session-manager-downloads/plugin/latest/ubuntu_64bit/session-manager-plugin.deb" -o session-manager-plugin.deb
94
+ sudo dpkg -i session-manager-plugin.deb
95
+ ```
96
+
97
+ ### Windows
98
+
99
+ Download the `.msi` or `.exe` installer from [GitHub Releases](https://github.com/yarka-guru/connection_app/releases).
100
+
101
+ Prerequisites must be installed separately: [aws-vault](https://github.com/99designs/aws-vault), [AWS CLI](https://aws.amazon.com/cli/), [Session Manager Plugin](https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html).
102
+
103
+ ### CLI (all platforms)
36
104
 
37
105
  ```bash
38
106
  npm install -g rds_ssm_connect
@@ -141,7 +209,7 @@ src/
141
209
  ## Publishing
142
210
 
143
211
  - **npm**: Published automatically via GitHub Actions when a release is created
144
- - **Desktop**: Multi-platform builds (macOS ARM64/x64, Linux x64, Windows x64) via `tauri-action` on git tags
212
+ - **Desktop**: Multi-platform builds (macOS ARM64/x64, Linux ARM64/x64, Windows x64) via `tauri-action` on git tags
145
213
 
146
214
  ## License
147
215
 
package/connect.js CHANGED
@@ -3,13 +3,14 @@
3
3
  import { exec } from 'node:child_process'
4
4
  import { EventEmitter } from 'node:events'
5
5
  import fs from 'node:fs/promises'
6
+ import net from 'node:net'
6
7
  import os from 'node:os'
7
8
  import path from 'node:path'
8
9
  import { promisify } from 'node:util'
9
10
  import { PROJECT_CONFIGS } from './envPortMapping.js'
10
11
 
11
12
  // Package info for version checking
12
- const packageJson = { name: 'rds_ssm_connect', version: '1.6.2' }
13
+ const packageJson = { name: 'rds_ssm_connect', version: '1.7.7' }
13
14
 
14
15
  const execAsync = promisify(exec)
15
16
 
@@ -22,6 +23,9 @@ const RETRY_CONFIG = {
22
23
  BASTION_WAIT_RETRY_DELAY_MS: 15000,
23
24
  PORT_FORWARDING_MAX_RETRIES: 2,
24
25
  SSM_AGENT_READY_WAIT_MS: 10000,
26
+ KEEPALIVE_INTERVAL_MS: 4 * 60 * 1000,
27
+ AUTO_RECONNECT_MAX_RETRIES: 50,
28
+ AUTO_RECONNECT_DELAY_MS: 3000,
25
29
  }
26
30
 
27
31
  // Version check configuration
@@ -119,6 +123,22 @@ async function sleep(ms) {
119
123
  return new Promise((resolve) => setTimeout(resolve, ms))
120
124
  }
121
125
 
126
+ // Keepalive: periodic TCP ping through the tunnel to prevent SSM idle timeout.
127
+ // Each connection attempt generates traffic on the SSM WebSocket channel,
128
+ // resetting the server-side idle timer (default 20 min).
129
+ function startKeepalive(localPort) {
130
+ const timer = setInterval(() => {
131
+ const socket = new net.Socket()
132
+ socket.setTimeout(5000)
133
+ socket.connect(parseInt(localPort, 10), '127.0.0.1', () => {
134
+ socket.destroy()
135
+ })
136
+ socket.on('error', () => socket.destroy())
137
+ socket.on('timeout', () => socket.destroy())
138
+ }, RETRY_CONFIG.KEEPALIVE_INTERVAL_MS)
139
+ return () => clearInterval(timer)
140
+ }
141
+
122
142
  async function terminateBastionInstance(ENV, instanceId, region) {
123
143
  const terminateCommand = `aws-vault exec ${ENV} -- aws ec2 terminate-instances --region ${region} --instance-ids ${instanceId}`
124
144
  await runCommand(terminateCommand)
@@ -487,7 +507,9 @@ async function getProfilesForProjectKey(projectKey) {
487
507
  return getProfilesForProject(allProfiles, projectConfig, PROJECT_CONFIGS)
488
508
  }
489
509
 
490
- // Connect to RDS through bastion - returns connection info and control object
510
+ // Connect to RDS through bastion - returns connection info and control object.
511
+ // Includes keepalive (prevents SSM idle timeout) and auto-reconnect
512
+ // (transparently reconnects on the same port if session drops unexpectedly).
491
513
  async function connect(projectKey, profile, options = {}) {
492
514
  const projectConfig = PROJECT_CONFIGS[projectKey]
493
515
  if (!projectConfig) {
@@ -498,6 +520,9 @@ async function connect(projectKey, profile, options = {}) {
498
520
  // Use provided localPort or fall back to computed port from profile
499
521
  const localPort = options.localPort || getLocalPort(profile, projectConfig)
500
522
 
523
+ let manualDisconnect = false
524
+ let stopKeepalive = null
525
+
501
526
  // Emit status updates
502
527
  const emit = (event, data) => {
503
528
  ipcEmitter.emit(event, data)
@@ -510,12 +535,12 @@ async function connect(projectKey, profile, options = {}) {
510
535
  const credentials = await getConnectionCredentials(profile, projectConfig)
511
536
 
512
537
  emit('status', { message: 'Finding bastion instance...' })
513
- const instanceId = await findBastionInstance(profile, region)
538
+ let currentInstanceId = await findBastionInstance(profile, region)
514
539
 
515
540
  emit('status', { message: 'Getting RDS endpoint...' })
516
- const rdsEndpoint = await getRdsEndpoint(profile, projectConfig)
541
+ let currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
517
542
 
518
- if (!rdsEndpoint || rdsEndpoint === 'None') {
543
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
519
544
  throw new Error('Failed to find the RDS endpoint.')
520
545
  }
521
546
 
@@ -528,29 +553,104 @@ async function connect(projectKey, profile, options = {}) {
528
553
  username: credentials.username,
529
554
  password: credentials.password,
530
555
  database,
531
- rdsEndpoint,
532
- instanceId,
556
+ rdsEndpoint: currentRdsEndpoint,
557
+ instanceId: currentInstanceId,
533
558
  }
534
559
 
535
560
  emit('credentials', connectionInfo)
536
561
  emit('status', { message: 'Starting port forwarding...' })
537
562
 
538
- // Start port forwarding
539
- const portForwardingPromise = startPortForwardingWithConfig(
540
- profile,
541
- instanceId,
542
- rdsEndpoint,
543
- localPort,
544
- rdsPort,
545
- region,
546
- 0,
547
- RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
548
- )
563
+ // Auto-reconnect session management loop.
564
+ // Keeps the tunnel alive on the SAME local port. Only exits on
565
+ // manual disconnect or after exhausting reconnection attempts.
566
+ const portForwardingPromise = (async () => {
567
+ let reconnectCount = 0
568
+
569
+ while (!manualDisconnect) {
570
+ try {
571
+ stopKeepalive = startKeepalive(localPort)
572
+
573
+ await startPortForwardingWithConfig(
574
+ profile,
575
+ currentInstanceId,
576
+ currentRdsEndpoint,
577
+ localPort,
578
+ rdsPort,
579
+ region,
580
+ 0,
581
+ RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES,
582
+ )
583
+
584
+ // Session ended — clean up keepalive
585
+ stopKeepalive?.()
586
+ stopKeepalive = null
587
+
588
+ if (manualDisconnect) break
589
+
590
+ // Unexpected disconnect (idle timeout, network issue) — auto-reconnect
591
+ reconnectCount++
592
+ if (reconnectCount > RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES) {
593
+ throw new Error('Maximum auto-reconnection attempts reached.')
594
+ }
595
+
596
+ emit('status', {
597
+ message: `Session ended. Reconnecting... (${reconnectCount})`,
598
+ })
599
+ await sleep(RETRY_CONFIG.AUTO_RECONNECT_DELAY_MS)
600
+
601
+ if (manualDisconnect) break
602
+
603
+ // Re-discover infrastructure (bastion may have been replaced by ASG)
604
+ emit('status', { message: 'Finding bastion instance...' })
605
+ currentInstanceId = await findBastionInstance(profile, region)
606
+
607
+ emit('status', { message: 'Getting RDS endpoint...' })
608
+ currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
609
+
610
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
611
+ throw new Error(
612
+ 'Failed to find the RDS endpoint during reconnection.',
613
+ )
614
+ }
615
+
616
+ emit('status', { message: 'Reconnecting port forwarding...' })
617
+ } catch (error) {
618
+ stopKeepalive?.()
619
+ stopKeepalive = null
620
+
621
+ if (manualDisconnect) break
622
+
623
+ reconnectCount++
624
+ if (reconnectCount > RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES) {
625
+ throw error
626
+ }
627
+
628
+ emit('status', {
629
+ message: `Connection error. Retrying... (${reconnectCount}/${RETRY_CONFIG.AUTO_RECONNECT_MAX_RETRIES})`,
630
+ })
631
+ await sleep(RETRY_CONFIG.AUTO_RECONNECT_DELAY_MS * 2)
632
+
633
+ if (manualDisconnect) break
634
+
635
+ try {
636
+ currentInstanceId = await findBastionInstance(profile, region)
637
+ currentRdsEndpoint = await getRdsEndpoint(profile, projectConfig)
638
+ if (!currentRdsEndpoint || currentRdsEndpoint === 'None') {
639
+ throw new Error('Failed to find RDS endpoint')
640
+ }
641
+ } catch (_innerError) {
642
+ // Will retry on next loop iteration
643
+ }
644
+ }
645
+ }
646
+ })()
549
647
 
550
648
  // Return connection control object
551
649
  return {
552
650
  connectionInfo,
553
651
  disconnect: () => {
652
+ manualDisconnect = true
653
+ stopKeepalive?.()
554
654
  // Kill all active child processes
555
655
  activeChildProcesses.forEach((child) => {
556
656
  if (child && !child.killed) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rds_ssm_connect",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -6,8 +6,8 @@
6
6
  * This script generates the update manifest that needs to be uploaded
7
7
  * to GitHub releases for auto-updates to work.
8
8
  *
9
- * Usage: node scripts/generate-update-json.js <version> <release-url>
10
- * Example: node scripts/generate-update-json.js 1.7.0 https://github.com/yarka-guru/connection_app/releases/download/v1.7.0
9
+ * Usage: node scripts/generate-update-json.js <version> <release-url> [sigs-dir]
10
+ * Example: node scripts/generate-update-json.js 1.7.0 https://github.com/yarka-guru/connection_app/releases/download/v1.7.0 sigs
11
11
  */
12
12
 
13
13
  import fs from 'node:fs'
@@ -18,6 +18,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
18
18
 
19
19
  const version = process.argv[2]
20
20
  const releaseUrl = process.argv[3]
21
+ const sigsDir = process.argv[4] // Optional: directory containing .sig files downloaded from release
21
22
 
22
23
  if (!version || !releaseUrl) {
23
24
  process.exit(1)
@@ -30,7 +31,38 @@ const tauriConf = JSON.parse(
30
31
  'utf-8',
31
32
  ),
32
33
  )
33
- const productName = tauriConf.productName.replace(/\s+/g, '_')
34
+ // Tauri uses dots for spaces in bundle filenames
35
+ const productName = tauriConf.productName.replace(/\s+/g, '.')
36
+
37
+ // Read signature from a .sig file (downloaded from release or local build)
38
+ function readSig(assetFilename) {
39
+ const sigFilename = `${assetFilename}.sig`
40
+
41
+ // Try sigs directory first (CI: downloaded from release assets)
42
+ if (sigsDir) {
43
+ try {
44
+ return fs.readFileSync(path.join(sigsDir, sigFilename), 'utf-8').trim()
45
+ } catch {}
46
+ }
47
+
48
+ // Try local build output
49
+ const bundleDir = path.join(__dirname, '../src-tauri/target/release/bundle')
50
+ for (const subdir of ['macos', 'appimage', 'nsis']) {
51
+ try {
52
+ return fs
53
+ .readFileSync(path.join(bundleDir, subdir, sigFilename), 'utf-8')
54
+ .trim()
55
+ } catch {}
56
+ }
57
+
58
+ return ''
59
+ }
60
+
61
+ const macAarch64 = `${productName}_aarch64.app.tar.gz`
62
+ const macX64 = `${productName}_x64.app.tar.gz`
63
+ const linuxAmd64 = `${productName}_${version}_amd64.AppImage`
64
+ const linuxAarch64 = `${productName}_${version}_aarch64.AppImage`
65
+ const windowsX64 = `${productName}_${version}_x64-setup.exe`
34
66
 
35
67
  const updateManifest = {
36
68
  version: version,
@@ -38,24 +70,24 @@ const updateManifest = {
38
70
  pub_date: new Date().toISOString(),
39
71
  platforms: {
40
72
  'darwin-aarch64': {
41
- url: `${releaseUrl}/${productName}_${version}_aarch64.app.tar.gz`,
42
- signature: '',
73
+ url: `${releaseUrl}/${macAarch64}`,
74
+ signature: readSig(macAarch64),
43
75
  },
44
76
  'darwin-x86_64': {
45
- url: `${releaseUrl}/${productName}_${version}_x64.app.tar.gz`,
46
- signature: '',
77
+ url: `${releaseUrl}/${macX64}`,
78
+ signature: readSig(macX64),
47
79
  },
48
80
  'linux-x86_64': {
49
- url: `${releaseUrl}/${productName}_${version}_amd64.AppImage.tar.gz`,
50
- signature: '',
81
+ url: `${releaseUrl}/${linuxAmd64}`,
82
+ signature: readSig(linuxAmd64),
51
83
  },
52
84
  'linux-aarch64': {
53
- url: `${releaseUrl}/${productName}_${version}_aarch64.AppImage.tar.gz`,
54
- signature: '',
85
+ url: `${releaseUrl}/${linuxAarch64}`,
86
+ signature: readSig(linuxAarch64),
55
87
  },
56
88
  'windows-x86_64': {
57
- url: `${releaseUrl}/${productName}_${version}_x64-setup.nsis.zip`,
58
- signature: '',
89
+ url: `${releaseUrl}/${windowsX64}`,
90
+ signature: readSig(windowsX64),
59
91
  },
60
92
  },
61
93
  }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "rds-ssm-connect"
3
- version = "1.7.5"
3
+ version = "1.7.7"
4
4
  description = "Secure RDS connections through AWS SSM"
5
5
  authors = ["Iaroslav Pyrogov"]
6
6
  edition = "2024"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://schema.tauri.app/config/2",
3
3
  "productName": "RDS SSM Connect",
4
- "version": "1.7.5",
4
+ "version": "1.7.7",
5
5
  "identifier": "com.rds-ssm-connect.desktop",
6
6
  "build": {
7
7
  "beforeDevCommand": "npm run dev:vite",
@@ -47,7 +47,7 @@
47
47
  "endpoints": [
48
48
  "https://github.com/yarka-guru/connection_app/releases/latest/download/latest.json"
49
49
  ],
50
- "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDI2MTFEQjk0MjUzRTgwQTEKUldTaGdENGxsTnNSSms1dTVvcWVKWUc2aFlOYVl5ODcvbVhiekVsVWxHbVlnRERSNjJsTHZPYkMK"
50
+ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEYxRjNDRkY1MkI1NTM2MzIKUldReU5sVXI5Yy96OGRNdjlHd3BqcTBkL0E4c2ZwUTRlL3VsZXVTdkNWRGxKL1A5ckNYNGdDNVoK"
51
51
  }
52
52
  }
53
53
  }