rds_ssm_connect 1.6.2 → 1.7.1

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 (40) hide show
  1. package/.github/workflows/npm-publish.yml +1 -1
  2. package/.github/workflows/release.yml +114 -0
  3. package/connect.js +203 -9
  4. package/envPortMapping.js +1 -1
  5. package/gui-adapter.js +268 -0
  6. package/index.html +21 -0
  7. package/package.json +23 -5
  8. package/scripts/generate-icons.js +206 -0
  9. package/scripts/generate-signing-key.sh +27 -0
  10. package/scripts/generate-update-json.js +61 -0
  11. package/scripts/pkg-sidecar-dev.js +94 -0
  12. package/scripts/pkg-sidecar.js +64 -0
  13. package/src/App.svelte +832 -0
  14. package/src/lib/ActiveConnections.svelte +468 -0
  15. package/src/lib/ConnectionForm.svelte +272 -0
  16. package/src/lib/CredentialsDisplay.svelte +400 -0
  17. package/src/lib/PrerequisitesCheck.svelte +241 -0
  18. package/src/lib/SavedConnections.svelte +559 -0
  19. package/src/lib/SessionStatus.svelte +102 -0
  20. package/src/lib/Settings.svelte +652 -0
  21. package/src/lib/UpdateBanner.svelte +167 -0
  22. package/src/main.js +8 -0
  23. package/src-tauri/Cargo.lock +5808 -0
  24. package/src-tauri/Cargo.toml +31 -0
  25. package/src-tauri/build.rs +3 -0
  26. package/src-tauri/capabilities/default.json +25 -0
  27. package/src-tauri/gen/schemas/acl-manifests.json +1 -0
  28. package/src-tauri/gen/schemas/capabilities.json +1 -0
  29. package/src-tauri/gen/schemas/desktop-schema.json +3025 -0
  30. package/src-tauri/gen/schemas/macOS-schema.json +3025 -0
  31. package/src-tauri/icons/128x128.png +0 -0
  32. package/src-tauri/icons/128x128@2x.png +0 -0
  33. package/src-tauri/icons/32x32.png +0 -0
  34. package/src-tauri/icons/icon.icns +0 -0
  35. package/src-tauri/icons/icon.ico +0 -0
  36. package/src-tauri/src/lib.rs +1143 -0
  37. package/src-tauri/src/main.rs +6 -0
  38. package/src-tauri/tauri.conf.json +53 -0
  39. package/svelte.config.js +5 -0
  40. package/vite.config.js +16 -0
@@ -18,4 +18,4 @@ jobs:
18
18
  registry-url: 'https://registry.npmjs.org'
19
19
  - run: npm install -g npm@latest
20
20
  - run: npm ci
21
- - run: npm publish --access public
21
+ - run: npm publish --provenance --access public
@@ -0,0 +1,114 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ env:
9
+ CARGO_INCREMENTAL: 0
10
+
11
+ jobs:
12
+ build:
13
+ permissions:
14
+ contents: write
15
+ strategy:
16
+ fail-fast: false
17
+ matrix:
18
+ include:
19
+ - platform: macos-latest
20
+ rust_target: aarch64-apple-darwin
21
+ sidecar_target: node22-macos-arm64
22
+ sidecar_triple: aarch64-apple-darwin
23
+ - platform: macos-latest
24
+ rust_target: x86_64-apple-darwin
25
+ sidecar_target: node22-macos-x64
26
+ sidecar_triple: x86_64-apple-darwin
27
+ - platform: ubuntu-22.04
28
+ rust_target: x86_64-unknown-linux-gnu
29
+ sidecar_target: node22-linux-x64
30
+ sidecar_triple: x86_64-unknown-linux-gnu
31
+ - platform: windows-latest
32
+ rust_target: x86_64-pc-windows-msvc
33
+ sidecar_target: node22-win-x64
34
+ sidecar_triple: x86_64-pc-windows-msvc
35
+
36
+ runs-on: ${{ matrix.platform }}
37
+ steps:
38
+ - uses: actions/checkout@v4
39
+
40
+ - name: Setup Node
41
+ uses: actions/setup-node@v4
42
+ with:
43
+ node-version: '22'
44
+ cache: 'npm'
45
+
46
+ - name: Install Rust stable
47
+ uses: dtolnay/rust-toolchain@stable
48
+ with:
49
+ targets: ${{ matrix.rust_target }}
50
+
51
+ - name: Rust cache
52
+ uses: Swatinem/rust-cache@v2
53
+ with:
54
+ workspaces: src-tauri
55
+
56
+ - name: Install dependencies (Ubuntu)
57
+ if: matrix.platform == 'ubuntu-22.04'
58
+ run: |
59
+ sudo apt-get update
60
+ sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf
61
+
62
+ - name: Install frontend dependencies
63
+ run: npm ci
64
+
65
+ - name: Build sidecar for current platform
66
+ run: |
67
+ mkdir -p src-tauri/binaries
68
+ npx esbuild gui-adapter.js --bundle --platform=node --target=node22 --outfile=src-tauri/binaries/gui-adapter-bundle.cjs --format=cjs --external:inquirer
69
+ npx @yao-pkg/pkg src-tauri/binaries/gui-adapter-bundle.cjs --target ${{ matrix.sidecar_target }} -o src-tauri/binaries/gui-adapter-${{ matrix.sidecar_triple }}
70
+ rm src-tauri/binaries/gui-adapter-bundle.cjs
71
+
72
+ - name: Build Tauri app
73
+ uses: tauri-apps/tauri-action@v0
74
+ env:
75
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
76
+ TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
77
+ TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
78
+ with:
79
+ tagName: v__VERSION__
80
+ releaseName: 'RDS SSM Connect v__VERSION__'
81
+ releaseBody: 'See the assets to download this version and install.'
82
+ releaseDraft: true
83
+ prerelease: false
84
+ args: --target ${{ matrix.rust_target }}
85
+
86
+ # Generate update manifest after all builds complete
87
+ update-manifest:
88
+ needs: build
89
+ runs-on: ubuntu-latest
90
+ permissions:
91
+ contents: write
92
+ steps:
93
+ - uses: actions/checkout@v4
94
+
95
+ - name: Setup Node
96
+ uses: actions/setup-node@v4
97
+ with:
98
+ node-version: '22'
99
+
100
+ - name: Get version from tag
101
+ id: version
102
+ run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
103
+
104
+ - name: Generate latest.json
105
+ run: |
106
+ node scripts/generate-update-json.js ${{ steps.version.outputs.VERSION }} https://github.com/${{ github.repository }}/releases/download/v${{ steps.version.outputs.VERSION }}
107
+
108
+ - name: Upload latest.json to release
109
+ uses: softprops/action-gh-release@v1
110
+ with:
111
+ files: latest.json
112
+ tag_name: v${{ steps.version.outputs.VERSION }}
113
+ env:
114
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
package/connect.js CHANGED
@@ -1,19 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import inquirer from 'inquirer'
4
3
  import fs from 'fs/promises'
5
4
  import os from 'os'
6
5
  import path from 'path'
7
6
  import { exec } from 'child_process'
8
7
  import { promisify } from 'util'
9
- import { createRequire } from 'module'
8
+ import { EventEmitter } from 'events'
10
9
  import { PROJECT_CONFIGS } from './envPortMapping.js'
11
10
 
12
- const require = createRequire(import.meta.url)
13
- const packageJson = require('./package.json')
11
+ // Package info for version checking
12
+ const packageJson = { name: 'rds_ssm_connect', version: '1.6.2' }
14
13
 
15
14
  const execAsync = promisify(exec)
16
15
 
16
+ // Event emitter for IPC communication
17
+ const ipcEmitter = new EventEmitter()
18
+
17
19
  // Configuration constants
18
20
  const RETRY_CONFIG = {
19
21
  BASTION_WAIT_MAX_RETRIES: 20,
@@ -317,7 +319,174 @@ function getProfilesForProject (allProfiles, projectConfig, allProjectConfigs) {
317
319
  }
318
320
  }
319
321
 
322
+ // Get local port number based on environment suffix
323
+ function getLocalPort (ENV, projectConfig) {
324
+ const { envPortMapping, defaultPort } = projectConfig
325
+ const allEnvSuffixes = Object.keys(envPortMapping).sort((a, b) => b.length - a.length)
326
+ const matchedSuffix = allEnvSuffixes.find(suffix => ENV.endsWith(suffix)) ||
327
+ allEnvSuffixes.find(suffix => ENV === suffix)
328
+ return envPortMapping[matchedSuffix] || defaultPort
329
+ }
330
+
331
+ // Get RDS credentials from Secrets Manager
332
+ async function getConnectionCredentials (ENV, projectConfig) {
333
+ const { region, secretPrefix, database } = projectConfig
334
+
335
+ const secretsListCommand = `aws-vault exec ${ENV} -- aws secretsmanager list-secrets --region ${region} --query "SecretList[?starts_with(Name, '${secretPrefix}')].Name | [0]" --output text`
336
+ const SECRET_NAME = await runCommand(secretsListCommand)
337
+
338
+ if (!SECRET_NAME || SECRET_NAME === 'None') {
339
+ throw new Error(`No secret found with name starting with '${secretPrefix}'.`)
340
+ }
341
+
342
+ const secretsGetCommand = `aws-vault exec ${ENV} -- aws secretsmanager get-secret-value --region ${region} --secret-id "${SECRET_NAME}" --query SecretString --output text`
343
+ const secretString = await runCommand(secretsGetCommand)
344
+
345
+ if (!secretString) {
346
+ throw new Error('Failed to retrieve secret value from Secrets Manager.')
347
+ }
348
+
349
+ let credentials
350
+ try {
351
+ credentials = JSON.parse(secretString)
352
+ if (!credentials.username || !credentials.password) {
353
+ throw new Error('Missing username or password in credentials')
354
+ }
355
+ } catch (error) {
356
+ throw new Error(`Failed to parse credentials from Secrets Manager: ${error.message}`)
357
+ }
358
+
359
+ return {
360
+ username: credentials.username,
361
+ password: credentials.password,
362
+ database,
363
+ secretName: SECRET_NAME
364
+ }
365
+ }
366
+
367
+ // Find running bastion instance
368
+ async function findBastionInstance (ENV, region) {
369
+ const instanceIdCommand = `aws-vault exec ${ENV} -- aws ec2 describe-instances --region ${region} --filters "Name=tag:Name,Values='*bastion*'" "Name=instance-state-name,Values=running" --query "Reservations[].Instances[].[InstanceId] | [0][0]" --output text`
370
+ const instanceId = await runCommand(instanceIdCommand)
371
+
372
+ if (!instanceId || instanceId === 'None') {
373
+ throw new Error('Failed to find a running instance with tag Name=*bastion*.')
374
+ }
375
+
376
+ return instanceId
377
+ }
378
+
379
+ // Get available projects based on AWS profiles
380
+ async function getAvailableProjects () {
381
+ const allProfiles = await readAwsConfig()
382
+
383
+ if (allProfiles.length === 0) {
384
+ return []
385
+ }
386
+
387
+ return Object.entries(PROJECT_CONFIGS)
388
+ .filter(([key, config]) => {
389
+ const matchingProfiles = getProfilesForProject(allProfiles, config, PROJECT_CONFIGS)
390
+ return matchingProfiles.length > 0
391
+ })
392
+ .map(([key, config]) => ({
393
+ key,
394
+ name: config.name
395
+ }))
396
+ }
397
+
398
+ // Get profiles for a specific project
399
+ async function getProfilesForProjectKey (projectKey) {
400
+ const allProfiles = await readAwsConfig()
401
+ const projectConfig = PROJECT_CONFIGS[projectKey]
402
+
403
+ if (!projectConfig) {
404
+ throw new Error(`Unknown project: ${projectKey}`)
405
+ }
406
+
407
+ return getProfilesForProject(allProfiles, projectConfig, PROJECT_CONFIGS)
408
+ }
409
+
410
+ // Connect to RDS through bastion - returns connection info and control object
411
+ async function connect (projectKey, profile, options = {}) {
412
+ const projectConfig = PROJECT_CONFIGS[projectKey]
413
+ if (!projectConfig) {
414
+ throw new Error(`Unknown project: ${projectKey}`)
415
+ }
416
+
417
+ const { region, database } = projectConfig
418
+ // Use provided localPort or fall back to computed port from profile
419
+ const localPort = options.localPort || getLocalPort(profile, projectConfig)
420
+
421
+ // Emit status updates
422
+ const emit = (event, data) => {
423
+ ipcEmitter.emit(event, data)
424
+ if (options.onEvent) {
425
+ options.onEvent(event, data)
426
+ }
427
+ }
428
+
429
+ emit('status', { message: 'Getting credentials...' })
430
+ const credentials = await getConnectionCredentials(profile, projectConfig)
431
+
432
+ emit('status', { message: 'Finding bastion instance...' })
433
+ const instanceId = await findBastionInstance(profile, region)
434
+
435
+ emit('status', { message: 'Getting RDS endpoint...' })
436
+ const rdsEndpoint = await getRdsEndpoint(profile, projectConfig)
437
+
438
+ if (!rdsEndpoint || rdsEndpoint === 'None') {
439
+ throw new Error('Failed to find the RDS endpoint.')
440
+ }
441
+
442
+ emit('status', { message: 'Getting RDS port...' })
443
+ const rdsPort = await getRdsPort(profile, projectConfig)
444
+
445
+ const connectionInfo = {
446
+ host: 'localhost',
447
+ port: localPort,
448
+ username: credentials.username,
449
+ password: credentials.password,
450
+ database,
451
+ rdsEndpoint,
452
+ instanceId
453
+ }
454
+
455
+ emit('credentials', connectionInfo)
456
+ emit('status', { message: 'Starting port forwarding...' })
457
+
458
+ // Start port forwarding
459
+ const portForwardingPromise = startPortForwardingWithConfig(
460
+ profile,
461
+ instanceId,
462
+ rdsEndpoint,
463
+ localPort,
464
+ rdsPort,
465
+ region,
466
+ 0,
467
+ RETRY_CONFIG.PORT_FORWARDING_MAX_RETRIES
468
+ )
469
+
470
+ // Return connection control object
471
+ return {
472
+ connectionInfo,
473
+ disconnect: () => {
474
+ // Kill all active child processes
475
+ activeChildProcesses.forEach(child => {
476
+ if (child && !child.killed) {
477
+ child.kill('SIGTERM')
478
+ }
479
+ })
480
+ activeChildProcesses = []
481
+ },
482
+ waitForClose: () => portForwardingPromise
483
+ }
484
+ }
485
+
320
486
  async function main () {
487
+ // Dynamic import of inquirer (CLI only, not needed for GUI adapter)
488
+ const inquirer = await import('inquirer')
489
+
321
490
  // Setup process cleanup handlers
322
491
  setupProcessCleanup()
323
492
 
@@ -355,7 +524,7 @@ async function main () {
355
524
  projectKey = projectChoices[0].value
356
525
  console.log(`Auto-selected project: ${projectChoices[0].name}`)
357
526
  } else {
358
- const projectAnswer = await inquirer.prompt([
527
+ const projectAnswer = await inquirer.default.prompt([
359
528
  {
360
529
  type: 'select',
361
530
  name: 'project',
@@ -377,7 +546,7 @@ async function main () {
377
546
  return
378
547
  }
379
548
 
380
- const envAnswer = await inquirer.prompt([
549
+ const envAnswer = await inquirer.default.prompt([
381
550
  {
382
551
  type: 'select',
383
552
  name: 'ENV',
@@ -462,6 +631,31 @@ async function main () {
462
631
  }
463
632
  }
464
633
 
465
- main().catch((error) => {
466
- console.error('Unhandled error in main function:', error)
467
- })
634
+ // Only run main() when executed directly (not when imported as a module)
635
+ const isMainModule = process.argv[1] && (
636
+ process.argv[1].endsWith('connect.js') ||
637
+ process.argv[1].endsWith('rds_ssm_connect')
638
+ )
639
+
640
+ if (isMainModule) {
641
+ main().catch((error) => {
642
+ console.error('Unhandled error in main function:', error)
643
+ })
644
+ }
645
+
646
+ // Exports for GUI adapter
647
+ export {
648
+ readAwsConfig,
649
+ getProfilesForProject,
650
+ getAvailableProjects,
651
+ getProfilesForProjectKey,
652
+ getConnectionCredentials,
653
+ findBastionInstance,
654
+ getRdsEndpoint,
655
+ getRdsPort,
656
+ getLocalPort,
657
+ connect,
658
+ ipcEmitter,
659
+ PROJECT_CONFIGS,
660
+ RETRY_CONFIG
661
+ }
package/envPortMapping.js CHANGED
@@ -35,7 +35,7 @@ export const PROJECT_CONFIGS = {
35
35
  covered: {
36
36
  name: 'Covered (Healthcare)',
37
37
  region: 'us-west-1',
38
- database: 'covered',
38
+ database: 'covered_db',
39
39
  secretPrefix: 'rds!db',
40
40
  rdsType: 'instance', // Single RDS instance (not Aurora)
41
41
  rdsPattern: 'covered-db', // DBInstanceIdentifier contains this
package/gui-adapter.js ADDED
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * GUI Adapter - JSON IPC bridge for Tauri sidecar
5
+ *
6
+ * Reads JSON commands from stdin, dispatches to connect.js functions,
7
+ * and writes JSON responses/events to stdout.
8
+ *
9
+ * Commands:
10
+ * - list-projects: Get available projects
11
+ * - list-profiles: Get profiles for a project
12
+ * - connect: Connect to RDS through bastion (supports multiple simultaneous connections)
13
+ * - disconnect: Disconnect a specific or all sessions
14
+ * - disconnect-all: Disconnect all active sessions
15
+ */
16
+
17
+ import readline from 'readline'
18
+ import net from 'net'
19
+ import { randomUUID } from 'crypto'
20
+ import {
21
+ getAvailableProjects,
22
+ getProfilesForProjectKey,
23
+ connect,
24
+ getLocalPort,
25
+ PROJECT_CONFIGS
26
+ } from './connect.js'
27
+
28
+ // Active connections Map - connectionId -> connection control object
29
+ const activeConnections = new Map()
30
+
31
+ // Send JSON response to stdout
32
+ function sendResponse (id, type, data) {
33
+ const response = JSON.stringify({ id, type, ...data })
34
+ console.log(response)
35
+ }
36
+
37
+ // Send event to stdout
38
+ function sendEvent (event, data) {
39
+ const response = JSON.stringify({ type: 'event', event, ...data })
40
+ console.log(response)
41
+ }
42
+
43
+ // Check if a port is available
44
+ async function isPortAvailable (port) {
45
+ return new Promise((resolve) => {
46
+ const server = net.createServer()
47
+ server.once('error', () => resolve(false))
48
+ server.once('listening', () => {
49
+ server.close()
50
+ resolve(true)
51
+ })
52
+ server.listen(port, '127.0.0.1')
53
+ })
54
+ }
55
+
56
+ // Find next available port starting from basePort
57
+ async function findAvailablePort (basePort, usedPorts = []) {
58
+ const usedSet = new Set(usedPorts.map(p => parseInt(p, 10)))
59
+
60
+ for (let port = basePort; port < basePort + 100; port++) {
61
+ if (usedSet.has(port)) continue
62
+ if (await isPortAvailable(port)) {
63
+ return port.toString()
64
+ }
65
+ }
66
+ throw new Error(`No available ports found starting from ${basePort}`)
67
+ }
68
+
69
+ // Handle incoming commands
70
+ async function handleCommand (command) {
71
+ const { id, action, ...params } = command
72
+
73
+ try {
74
+ switch (action) {
75
+ case 'list-projects': {
76
+ const projects = await getAvailableProjects()
77
+ sendResponse(id, 'success', { projects })
78
+ break
79
+ }
80
+
81
+ case 'list-profiles': {
82
+ const { projectKey } = params
83
+ if (!projectKey) {
84
+ sendResponse(id, 'error', { message: 'projectKey is required' })
85
+ break
86
+ }
87
+ const profiles = await getProfilesForProjectKey(projectKey)
88
+ sendResponse(id, 'success', { profiles })
89
+ break
90
+ }
91
+
92
+ case 'connect': {
93
+ const { projectKey, profile, localPort, usedPorts = [] } = params
94
+ if (!projectKey || !profile) {
95
+ sendResponse(id, 'error', { message: 'projectKey and profile are required' })
96
+ break
97
+ }
98
+
99
+ // Generate unique connection ID
100
+ const connectionId = `conn_${randomUUID().slice(0, 8)}`
101
+
102
+ // Determine port to use
103
+ const projectConfig = PROJECT_CONFIGS[projectKey]
104
+ if (!projectConfig) {
105
+ sendResponse(id, 'error', { message: `Unknown project: ${projectKey}` })
106
+ break
107
+ }
108
+
109
+ let portToUse
110
+ if (localPort) {
111
+ // Use specified port if available
112
+ const portNum = parseInt(localPort, 10)
113
+ if (usedPorts.includes(localPort) || !(await isPortAvailable(portNum))) {
114
+ sendResponse(id, 'error', { message: `Port ${localPort} is not available` })
115
+ break
116
+ }
117
+ portToUse = localPort
118
+ } else {
119
+ // Find next available port based on project's base port
120
+ const basePort = parseInt(getLocalPort(profile, projectConfig), 10)
121
+ // Combine usedPorts from params with currently active connections
122
+ const allUsedPorts = [
123
+ ...usedPorts,
124
+ ...Array.from(activeConnections.values()).map(c => c.connectionInfo?.port)
125
+ ].filter(Boolean)
126
+ portToUse = await findAvailablePort(basePort, allUsedPorts)
127
+ }
128
+
129
+ // Start new connection with the determined port
130
+ const connectionControl = await connect(projectKey, profile, {
131
+ localPort: portToUse,
132
+ onEvent: (event, data) => {
133
+ sendEvent(event, { ...data, connectionId })
134
+ }
135
+ })
136
+
137
+ // Store in active connections
138
+ activeConnections.set(connectionId, connectionControl)
139
+
140
+ sendResponse(id, 'success', {
141
+ connectionId,
142
+ connectionInfo: connectionControl.connectionInfo
143
+ })
144
+
145
+ // Set up connection close handler
146
+ connectionControl.waitForClose()
147
+ .then(() => {
148
+ sendEvent('disconnected', { connectionId, reason: 'session_ended' })
149
+ activeConnections.delete(connectionId)
150
+ })
151
+ .catch((error) => {
152
+ sendEvent('error', { connectionId, message: error.message })
153
+ sendEvent('disconnected', { connectionId, reason: 'error' })
154
+ activeConnections.delete(connectionId)
155
+ })
156
+
157
+ break
158
+ }
159
+
160
+ case 'disconnect': {
161
+ const { connectionId } = params
162
+
163
+ if (connectionId) {
164
+ // Disconnect specific connection
165
+ const connection = activeConnections.get(connectionId)
166
+ if (connection) {
167
+ connection.disconnect()
168
+ activeConnections.delete(connectionId)
169
+ sendResponse(id, 'success', { message: `Disconnected ${connectionId}` })
170
+ } else {
171
+ sendResponse(id, 'success', { message: `Connection ${connectionId} not found` })
172
+ }
173
+ } else {
174
+ // Disconnect all connections (legacy behavior)
175
+ for (const [connId, connection] of activeConnections) {
176
+ connection.disconnect()
177
+ }
178
+ activeConnections.clear()
179
+ sendResponse(id, 'success', { message: 'Disconnected all connections' })
180
+ }
181
+ break
182
+ }
183
+
184
+ case 'disconnect-all': {
185
+ for (const [connId, connection] of activeConnections) {
186
+ connection.disconnect()
187
+ }
188
+ activeConnections.clear()
189
+ sendResponse(id, 'success', { message: 'Disconnected all connections' })
190
+ break
191
+ }
192
+
193
+ case 'status': {
194
+ const connections = Array.from(activeConnections.entries()).map(([connId, conn]) => ({
195
+ connectionId: connId,
196
+ connectionInfo: conn.connectionInfo
197
+ }))
198
+ sendResponse(id, 'success', {
199
+ connectionCount: activeConnections.size,
200
+ connections
201
+ })
202
+ break
203
+ }
204
+
205
+ case 'ping': {
206
+ sendResponse(id, 'success', { message: 'pong' })
207
+ break
208
+ }
209
+
210
+ default:
211
+ sendResponse(id, 'error', { message: `Unknown action: ${action}` })
212
+ }
213
+ } catch (error) {
214
+ sendResponse(id, 'error', { message: error.message })
215
+ }
216
+ }
217
+
218
+ // Handle process signals for cleanup
219
+ function setupCleanup () {
220
+ const cleanup = () => {
221
+ for (const [connId, connection] of activeConnections) {
222
+ connection.disconnect()
223
+ }
224
+ activeConnections.clear()
225
+ process.exit(0)
226
+ }
227
+
228
+ process.on('SIGINT', cleanup)
229
+ process.on('SIGTERM', cleanup)
230
+ process.on('exit', cleanup)
231
+ }
232
+
233
+ // Main entry point
234
+ async function main () {
235
+ setupCleanup()
236
+
237
+ // Signal that adapter is ready
238
+ sendEvent('ready', { version: '2.0.0' })
239
+
240
+ // Set up readline for stdin
241
+ const rl = readline.createInterface({
242
+ input: process.stdin,
243
+ output: process.stdout,
244
+ terminal: false
245
+ })
246
+
247
+ rl.on('line', async (line) => {
248
+ try {
249
+ const command = JSON.parse(line)
250
+ await handleCommand(command)
251
+ } catch (error) {
252
+ sendEvent('error', { message: `Failed to parse command: ${error.message}` })
253
+ }
254
+ })
255
+
256
+ rl.on('close', () => {
257
+ for (const [connId, connection] of activeConnections) {
258
+ connection.disconnect()
259
+ }
260
+ activeConnections.clear()
261
+ process.exit(0)
262
+ })
263
+ }
264
+
265
+ main().catch((error) => {
266
+ sendEvent('error', { message: `Adapter error: ${error.message}` })
267
+ process.exit(1)
268
+ })
package/index.html ADDED
@@ -0,0 +1,21 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>RDS SSM Connect</title>
7
+ <style>
8
+ html, body {
9
+ margin: 0;
10
+ padding: 0;
11
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
12
+ background-color: #1a1a2e;
13
+ color: #eaeaea;
14
+ }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div id="app"></div>
19
+ <script type="module" src="/src/main.js"></script>
20
+ </body>
21
+ </html>