Skip to content

Repository files navigation

MCAStation

Mac Cloud Access Station — a macOS desktop app for managing database tunnels to AWS across projects. SSH bastions today, AWS SSM Session Manager as the migration target.

Explained by Lenzon

See docs/bastion-manager-spec.md for the full architecture and the phased roadmap; §14 tracks per-phase progress in detail.

Status: Phase 3 complete — selector, capabilities, key custody

Phase 1 — core switcher (SSH). Verified end-to-end against real AWS.

  • SQLite config store (better-sqlite3), projects + environments, seeded with one example project (dev/prod) on first run.
  • TunnelManager built on the ConnectionStrategy seam, with SshStrategy live and SsmStrategy stubbed for Phase 3.
  • Per-environment state machine (stopped → starting_bastion → bastion_ready → tunneling → connected / error), PID tracking, and kill-all-tunnels on quit.
  • TCP health check on the local port.
  • Two-pane React UI: project/environment list with phase glyphs + SSH/SSM badges, environment detail with controls, live telemetry, and a log console.

Phase 2 — hybrid ports + secrets. Typechecks and lints clean, packages, and boots; not yet exercised live against AWS.

  • Pin/un-pin singleton on 5432. ConfigStore.effectivePort(env) is the one source of truth for which port an environment is on. Pinning un-pins the previous holder and restarts live tunnels onto the right port; a TCP guard refuses to pin when 5432 is held by a non-managed process.
  • Multi-tunnel coexistence — dev on 5432 and prod on 5433 side by side, falling out of the effective-port model.
  • Secrets Manager → connection string. Builds postgresql://…@localhost:<effectivePort>/<db> from the RDS-standard secret JSON. Clipboard-only, auto-clears after ~30s, never persisted, logged, or returned raw over IPC.
  • "Check account"sts get-caller-identity against the project's resolved profile, so you can see which account you're about to touch.
  • UI: pin star + toggle, "Copy connection string", and an account card.

Phase 3 — project selector, capabilities, key custody. Typechecks and lints clean and packages; the AWS-side account plumbing (§3.1 of the Phase 3 plan) is operator work that hasn't been done yet, so key custody is unexercised.

  • Capability registry (src/shared/capabilities.ts) — the catalog lives in code; the project_capability table only records which projects have which enabled. The renderer renders the intersection, so a DB row naming a capability this binary doesn't have is ignored rather than fatal.
  • Project selector + capability tabs. The left rail is now scoped to one project at a time; selection persists in ui_state across restarts.
  • Global 5432 chip in the top bar. Because pin_state is one row app-wide, the pin holder may belong to a project you aren't looking at — the chip always names its true owner, cross-project, in a warning tone when it's elsewhere.
  • Key custody without keys on disk. ssh_key_secret_id on an environment wins over ssh_key_path. KeyAgentService fetches the private key from Secrets Manager, pipes it into an app-owned ssh-agent via ssh-add -t <ttl> - (stdin, never a file), and SshStrategy runs against that agent with IdentitiesOnly=yes so it can't fall back to ~/.ssh. Agents are reaped on tunnel stop and on quit.
  • Secrets panel — a read-only second capability, deliberately narrow: it copies the connection string for environments that have a secret, and does not browse or edit Secrets Manager.

Not yet (later phases): SSM strategy, generic command runner, runbooks.

Requirements

  • Node 22+ (Electron 43 requires it). Installed here via brew install node@22. See .nvmrc.

Run in development

Node 22 is keg-only, so point at it explicitly (or add it to PATH):

export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
npm start

The first npm start rebuilds the native better-sqlite3 module against Electron's ABI. If it ever gets out of sync (e.g. after upgrading Electron):

npx electron-rebuild -f -w better-sqlite3

Run as a real app

You don't need VS Code open to use this. Package it and drop it in /Applications:

export PATH="/opt/homebrew/opt/node@22/bin:$PATH"
npm run app        # package, then replace /Applications/MCAStation.app

First launch only: right-click → Open to clear Gatekeeper (the build is unsigned). After that it's Spotlight-launchable like anything else.

The SQLite path comes from app.getPath('userData'), which resolves the same for packaged and dev builds — your config carries over rather than starting fresh.

Note on packaging: the Vite plugin only copies .vite/ and package.json into the bundle, but better-sqlite3 is deliberately external (a .node binary can't be bundled). Before this phase the packaged app therefore launched straight into Cannot find module 'better-sqlite3' while npm start worked — dev resolved it from the repo's own node_modules. A packageAfterCopy hook in forge.config.ts now copies the module in with its Electron-ABI build, and fails the build if the binary is missing rather than shipping a broken app.

Project layout

src/
  shared/     types + IPC contract (spoken by both processes)
  main/       privileged backend — the only place that spawns ssh/aws
    config/   SQLite: db bootstrap, ConfigStore, first-run seed
    tunnel/   ConnectionStrategy, SshStrategy, SsmStrategy(stub),
              TunnelManager, awsService, secret masking
    health/   HealthChecker (TCP probe)
    ipc/      registerHandlers — wires the contract to the services
  preload/    contextBridge — exposes only window.bastion to the UI
  renderer/   React UI (App, EnvironmentForm, PhaseGlyph)

Config location

The SQLite database lives at:

~/Library/Application Support/MCAStation/config.db

Nothing sensitive is stored there — no passwords, no secret values, no bastion IPs (those are fetched live). secret_id is only a pointer into Secrets Manager.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages