Skip to content

Use secure websockets with mkcert#586

Open
huntercaron wants to merge 8 commits intomainfrom
huntercaron/mkcert-secure-ws
Open

Use secure websockets with mkcert#586
huntercaron wants to merge 8 commits intomainfrom
huntercaron/mkcert-secure-ws

Conversation

@huntercaron
Copy link
Collaborator

@huntercaron huntercaron commented Mar 6, 2026

Description

This PR adds TLS certificate generation and management to the Code Link CLI, enabling secure WebSocket (WSS) connections. Certificates are automatically generated on first run and cached locally in ~/.framer/code-link/. The CA is automatically installed to the system keychain on macOS with a one-time password prompt.

The certificate setup message has been improved for clarity and styled consistently with other status messages.

Changelog

  • Added mkcert integration for local certificate generation
  • Automatic CA installation to macOS system keychain
  • Certificate caching across CLI runs
  • Improved CLI messaging for certificate setup
  • Made TLS mandatory on the server (removed plain WS fallback that was incompatible with the WSS-only client)

Testing

  • Run the plugin in Safari and connect to the CLI — verify the connection works over WSS
  • Verify the CLI informs you why you are asked for your password (CA installation)

Safari silently blocks ws:// connections on HTTPS pages without firing
the close event, leaving the plugin stuck in "loading". Hardcode the
protocol to wss:// so failures increment failureCount normally and the
disconnected InfoPanel appears after 2 failures.
Generate self-signed certs via mkcert so the CLI serves WSS, matching
the plugin's wss:// protocol. Falls back to plain WS with a warning if
cert generation fails.
@github-actions github-actions bot added the Auto submit to Marketplace on merge Submits the plugin to the marketplace after merging label Mar 6, 2026
Remove ws:// fallback entirely — the CLI now requires TLS certs and
fails with a clear error message instead of silently downgrading. When
a new CA is generated (e.g. after the user deletes ca.key/ca.crt), the
old server cert and install marker are cleaned up so they get
regenerated against the new CA.
On Linux and Windows, tryInstallCA silently returned without warning,
leaving users with no explanation for why WSS connections fail. Now
shows platform-specific manual installation instructions.
@huntercaron huntercaron removed the Auto submit to Marketplace on merge Submits the plugin to the marketplace after merging label Mar 6, 2026
@huntercaron huntercaron changed the title Add mkcert TLS support to CLI for WSS connections Use secure websockets with mkcert Mar 6, 2026
Copy link

Copilot AI commented Mar 6, 2026

@Nick-Lucas I've opened a new pull request, #588, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds TLS certificate generation/management to enable secure WebSocket (WSS) connections between the Framer plugin and the Code Link CLI, making TLS mandatory for local connections.

Changes:

  • Plugin switches client connections from ws:// to wss://.
  • CLI WebSocket server is now hosted over an HTTPS (TLS) server using locally generated certificates.
  • New CLI helper to download/run mkcert, cache certs, and (on macOS) install the CA into the system trust store.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 6 comments.

File Description
plugins/code-link/src/utils/sockets.ts Forces plugin WebSocket client to use wss:// and logs protocol on close.
packages/code-link-cli/src/helpers/connection.ts Wraps ws server with an HTTPS server to provide WSS-only connections.
packages/code-link-cli/src/helpers/certs.ts New mkcert-based certificate download/generation/cache helper for localhost TLS.
packages/code-link-cli/src/controller.ts Generates/loads certs before starting the WSS server; improves failure messaging.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


async function ensureMkcertBinary(): Promise<string> {
try {
await fs.access(MKCERT_BIN_PATH, fs.constants.X_OK)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fs is imported from fs/promises, which does not consistently expose constants. Using fs.constants.X_OK here can throw at runtime. Import constants from node:fs (or import fs from "node:fs" alongside the promises API) and use that for the access mode, or switch to a stat/chmod-based check.

Copilot uses AI. Check for mistakes.
Comment on lines +118 to +130
const url = getDownloadUrl()
debug(`Downloading mkcert from ${url}`)
status("Downloading mkcert for certificate generation...")

try {
const response = await fetch(url, { redirect: "follow" })
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}

const buffer = Buffer.from(await response.arrayBuffer())
await fs.writeFile(MKCERT_BIN_PATH, buffer, { mode: 0o755 })
debug(`mkcert binary saved to ${MKCERT_BIN_PATH}`)
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mkcert binary is downloaded and executed without any integrity verification (checksum/signature). This creates a supply-chain risk if the download is intercepted or the release asset is tampered with. Consider pinning and verifying a published checksum for the exact asset, or requiring users to install mkcert via their package manager and only shelling out to an already-present binary.

Copilot uses AI. Check for mistakes.
} catch (err) {
// Non-fatal — certs might still work, but the browser may not auto-trust them.
warn(`Could not install CA into system trust store: ${err instanceof Error ? err.message : err}`)
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateCerts treats any mkcert failure as “non-fatal” and logs it as a CA-install problem, but the same exec also generates the server cert/key. If mkcert fails before writing the files, the later reads will fail with a less-direct error path. Consider validating that localhost-key.pem and localhost.pem exist after this step and throwing a fatal error if they weren’t generated; alternatively split CA install and cert generation so generation failures are handled explicitly.

Suggested change
}
}
// Regardless of mkcert's exit status, ensure the server key/cert were actually generated.
const [key, cert] = await Promise.all([
loadFile(SERVER_KEY_PATH),
loadFile(SERVER_CERT_PATH),
])
if (!key || !cert) {
throw new Error(
"Failed to generate localhost TLS certificate and key with mkcert. " +
"Please ensure mkcert is installed and rerun:\n" +
` mkcert -install && mkcert -key-file "${SERVER_KEY_PATH}" -cert-file "${SERVER_CERT_PATH}" localhost 127.0.0.1`
)
}

Copilot uses AI. Check for mistakes.
Comment on lines +38 to 44
// Create WSS server
const httpsServer = https.createServer({ key: certs.key, cert: certs.cert })
const wss = new WebSocketServer({ server: httpsServer })

const handleError = (err: NodeJS.ErrnoException) => {
if (!isReady) {
// Startup error - reject the promise with a helpful message
if (err.code === "EADDRINUSE") {
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WebSocketServer can emit its own error events; currently only the underlying httpsServer has an error listener. If wss emits an error without a listener, Node will treat it as an unhandled error event and crash the process. Add a wss.on("error", ...) handler (and ideally forward to handlers.onError) in addition to the HTTPS server error handling.

Copilot uses AI. Check for mistakes.
Comment on lines +50 to +75
export async function getOrCreateCerts(): Promise<CertBundle | null> {
try {
await fs.mkdir(CERT_DIR, { recursive: true })

// Fast path: certs already exist on disk
const existingKey = await loadFile(SERVER_KEY_PATH)
const existingCert = await loadFile(SERVER_CERT_PATH)

if (existingKey && existingCert) {
debug("Loaded existing server certificates from disk")
return { key: existingKey, cert: existingCert }
}

// Slow path: download mkcert (if needed) and generate certs
const mkcertPath = await ensureMkcertBinary()
await retainExistingCA(mkcertPath)
status("Generating local certificates to connect securely. You may be asked for your password.")
await generateCerts(mkcertPath)

const key = await fs.readFile(SERVER_KEY_PATH, "utf-8")
const cert = await fs.readFile(SERVER_CERT_PATH, "utf-8")
return { key, cert }
} catch (err) {
error(`Failed to set up TLS certificates: ${err instanceof Error ? err.message : err}`)
return null
}
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New certificate management logic is substantial (download/caching, platform mapping, CA retention) but currently has no automated coverage. Since this package already uses Vitest, add unit tests for the pure parts (e.g. download URL selection, cache fast-path behavior, and error handling when files are missing) and mock out execFile/fetch to avoid running mkcert in tests.

Copilot uses AI. Check for mistakes.
Comment on lines +1072 to +1075
error("To fix this:")
error(" 1. Re-run this command — certificate generation is often a one-time issue")
error(" 2. Manually delete ~/.framer/code-link/ and try again")
error("")
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The recovery instructions hardcode a Unix-style path (~/.framer/code-link/). On Windows this is incorrect and can confuse users. Consider printing the actual resolved cert directory (built from os.homedir() / the same constant used by the cert helper) and/or tailoring instructions per platform.

Copilot uses AI. Check for mistakes.
@Nick-Lucas
Copy link
Contributor

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

// Non-fatal — certs might still work, but the browser may not auto-trust them.
warn(`Could not install CA into system trust store: ${err instanceof Error ? err.message : err}`)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateCerts swallows fatal cert generation failures as non-fatal

Medium Severity

generateCerts combines -install (CA trust store installation) and certificate generation in a single mkcert invocation, but the catch block swallows all errors as non-fatal. On macOS, when the user cancels the password prompt, mkcert exits with status 1 before generating cert files. Because the catch only warns ("certs might still work"), the caller proceeds to fs.readFile the non-existent cert files, producing a confusing ENOENT error. The comment "Non-fatal — certs might still work" is incorrect since both operations share a single process exit code — a fatal -install failure prevents cert generation entirely.

Additional Locations (1)

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants