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.
- package/.github/workflows/npm-publish.yml +1 -1
- package/.github/workflows/release.yml +114 -0
- package/connect.js +203 -9
- package/envPortMapping.js +1 -1
- package/gui-adapter.js +268 -0
- package/index.html +21 -0
- package/package.json +23 -5
- package/scripts/generate-icons.js +206 -0
- package/scripts/generate-signing-key.sh +27 -0
- package/scripts/generate-update-json.js +61 -0
- package/scripts/pkg-sidecar-dev.js +94 -0
- package/scripts/pkg-sidecar.js +64 -0
- package/src/App.svelte +832 -0
- package/src/lib/ActiveConnections.svelte +468 -0
- package/src/lib/ConnectionForm.svelte +272 -0
- package/src/lib/CredentialsDisplay.svelte +400 -0
- package/src/lib/PrerequisitesCheck.svelte +241 -0
- package/src/lib/SavedConnections.svelte +559 -0
- package/src/lib/SessionStatus.svelte +102 -0
- package/src/lib/Settings.svelte +652 -0
- package/src/lib/UpdateBanner.svelte +167 -0
- package/src/main.js +8 -0
- package/src-tauri/Cargo.lock +5808 -0
- package/src-tauri/Cargo.toml +31 -0
- package/src-tauri/build.rs +3 -0
- package/src-tauri/capabilities/default.json +25 -0
- package/src-tauri/gen/schemas/acl-manifests.json +1 -0
- package/src-tauri/gen/schemas/capabilities.json +1 -0
- package/src-tauri/gen/schemas/desktop-schema.json +3025 -0
- package/src-tauri/gen/schemas/macOS-schema.json +3025 -0
- package/src-tauri/icons/128x128.png +0 -0
- package/src-tauri/icons/128x128@2x.png +0 -0
- package/src-tauri/icons/32x32.png +0 -0
- package/src-tauri/icons/icon.icns +0 -0
- package/src-tauri/icons/icon.ico +0 -0
- package/src-tauri/src/lib.rs +1143 -0
- package/src-tauri/src/main.rs +6 -0
- package/src-tauri/tauri.conf.json +53 -0
- package/svelte.config.js +5 -0
- package/vite.config.js +16 -0
|
@@ -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 {
|
|
8
|
+
import { EventEmitter } from 'events'
|
|
10
9
|
import { PROJECT_CONFIGS } from './envPortMapping.js'
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
const packageJson =
|
|
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()
|
|
466
|
-
|
|
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: '
|
|
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>
|