Conversation
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.
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.
|
@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. |
There was a problem hiding this comment.
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://towss://. - 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) |
There was a problem hiding this comment.
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.
| 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}`) |
There was a problem hiding this comment.
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.
| } 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}`) | ||
| } |
There was a problem hiding this comment.
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.
| } | |
| } | |
| // 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` | |
| ) | |
| } |
| // 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") { |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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("") |
There was a problem hiding this comment.
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.
|
@cursor review |
There was a problem hiding this comment.
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}`) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.


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
Testing