Skip to content

Commit bbe05fe

Browse files
committed
feat: store session tokens in the OS keyring
On macOS and Windows with CLI >= 2.29.0, write session tokens to the OS keyring (Keychain / Credential Manager) instead of plaintext files. The CLI reads from the keyring when invoked with --url instead of --global-config. Falls back to file storage on Linux, older CLIs, or if the keyring write fails. Key changes: - Add KeyringStore wrapping @napi-rs/keyring with the CLI's credential format (JSON map keyed by host, base64 on macOS, raw bytes on Windows) - Add CliAuth discriminated union ("global-config" | "url") threaded through proxy command building and workspace state machine - Add shouldUseKeyring() as single source of truth gating on CLI version, platform, and coder.useKeyring setting - Restructure remote.ts setup() to call configure() after featureSet is known, so the keyring decision can be made - Add keyring read fallback in LoginCoordinator for tokens written by `coder login` from the terminal - Add vendor-keyring.mjs build script to copy native binaries into dist/node_modules/ for VSIX packaging (vsce can't follow pnpm symlinks) - Harden file fallback with mode 0o600
1 parent ea84a40 commit bbe05fe

23 files changed

+1115
-71
lines changed

.npmrc

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# Install @napi-rs/keyring native binaries for macOS and Windows so they're
2+
# available when building the universal VSIX (even on Linux CI).
3+
# Only macOS and Windows use the keyring; Linux falls back to file storage.
4+
supportedArchitectures.os[]=current
5+
supportedArchitectures.os[]=darwin
6+
supportedArchitectures.os[]=win32
7+
supportedArchitectures.cpu[]=current
8+
supportedArchitectures.cpu[]=x64
9+
supportedArchitectures.cpu[]=arm64

esbuild.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const buildOptions = {
3232
// undefined when bundled to CJS, causing runtime errors.
3333
openpgp: "./node_modules/openpgp/dist/node/openpgp.min.cjs",
3434
},
35-
external: ["vscode"],
35+
external: ["vscode", "@napi-rs/keyring"],
3636
sourcemap: production ? "external" : true,
3737
minify: production,
3838
plugins: watch ? [logRebuildPlugin] : [],

eslint.config.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export default defineConfig(
158158

159159
// Build config - ESM with Node globals
160160
{
161-
files: ["esbuild.mjs"],
161+
files: ["esbuild.mjs", "scripts/*.mjs"],
162162
languageOptions: {
163163
globals: {
164164
...globals.node,

package.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
"test:integration": "tsc -p test --outDir out --noCheck && node esbuild.mjs && vscode-test",
3333
"test:webview": "vitest --project webview",
3434
"typecheck": "concurrently -g \"tsc --noEmit\" \"tsc --noEmit -p test\"",
35-
"vscode:prepublish": "pnpm build:production",
35+
"vscode:prepublish": "pnpm build:production && node scripts/vendor-keyring.mjs",
3636
"watch": "concurrently -n extension,webviews \"pnpm watch:extension\" \"pnpm watch:webviews\"",
3737
"watch:extension": "node esbuild.mjs --watch",
3838
"watch:webviews": "pnpm -r --filter \"./packages/*\" --parallel dev"
@@ -164,6 +164,11 @@
164164
"type": "string"
165165
}
166166
},
167+
"coder.useKeyring": {
168+
"markdownDescription": "Store session tokens in the OS keyring (macOS Keychain, Windows Credential Manager) instead of plaintext files. Requires CLI >= 2.29.0. Has no effect on Linux.",
169+
"type": "boolean",
170+
"default": true
171+
},
167172
"coder.httpClientLogLevel": {
168173
"markdownDescription": "Controls the verbosity of HTTP client logging. This affects what details are logged for each HTTP request and response.",
169174
"type": "string",
@@ -471,6 +476,7 @@
471476
"word-wrap": "1.2.5"
472477
},
473478
"dependencies": {
479+
"@napi-rs/keyring": "^1.2.0",
474480
"@peculiar/x509": "^1.14.3",
475481
"@repo/shared": "workspace:*",
476482
"axios": "1.13.5",

pnpm-lock.yaml

Lines changed: 139 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

scripts/vendor-keyring.mjs

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/**
2+
* Vendor @napi-rs/keyring into dist/node_modules/ for VSIX packaging.
3+
*
4+
* pnpm uses symlinks that vsce can't follow. This script resolves them and
5+
* copies the JS wrapper plus macOS/Windows .node binaries into dist/, where
6+
* Node's require() resolution finds them from dist/extension.js.
7+
*/
8+
import { cpSync, existsSync, mkdirSync, realpathSync, rmSync } from "node:fs";
9+
import { join, resolve, basename } from "node:path";
10+
11+
const outputDir = resolve("dist/node_modules/@napi-rs/keyring");
12+
const keyringPkg = resolve("node_modules/@napi-rs/keyring");
13+
14+
if (!existsSync(keyringPkg)) {
15+
console.log("@napi-rs/keyring not found, skipping");
16+
process.exit(0);
17+
}
18+
19+
const resolvedPkg = realpathSync(keyringPkg);
20+
21+
rmSync(outputDir, { recursive: true, force: true });
22+
mkdirSync(outputDir, { recursive: true });
23+
cpSync(resolvedPkg, outputDir, { recursive: true });
24+
25+
// Platform packages are siblings of the resolved keyring package in pnpm's layout.
26+
// Exact file names so the build fails loudly if the native module renames anything.
27+
const siblingsDir = resolve(resolvedPkg, "..");
28+
const binaries = [
29+
"keyring-darwin-arm64/keyring.darwin-arm64.node",
30+
"keyring-darwin-x64/keyring.darwin-x64.node",
31+
"keyring-win32-arm64-msvc/keyring.win32-arm64-msvc.node",
32+
"keyring-win32-x64-msvc/keyring.win32-x64-msvc.node",
33+
];
34+
35+
for (const binary of binaries) {
36+
const symlink = join(siblingsDir, binary);
37+
if (!existsSync(symlink)) {
38+
console.error(
39+
`Missing native binary: ${binary}\n` +
40+
"Ensure .npmrc includes supportedArchitectures for all target OS/CPU combinations.",
41+
);
42+
process.exit(1);
43+
}
44+
const src = realpathSync(symlink);
45+
const filename = basename(binary);
46+
const dest = join(outputDir, filename);
47+
cpSync(src, dest);
48+
}
49+
50+
console.log(
51+
`Vendored @napi-rs/keyring with ${binaries.length} platform binaries into dist/`,
52+
);

src/api/workspace.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
import { spawn } from "node:child_process";
88
import * as vscode from "vscode";
99

10-
import { getGlobalFlags } from "../cliConfig";
10+
import { type CliAuth, getGlobalFlags } from "../cliConfig";
1111
import { type FeatureSet } from "../featureSet";
1212
import { escapeCommandArg } from "../util";
1313
import { type UnidirectionalStream } from "../websocket/eventStreamConnection";
@@ -50,7 +50,7 @@ export class LazyStream<T> {
5050
*/
5151
export async function startWorkspaceIfStoppedOrFailed(
5252
restClient: Api,
53-
globalConfigDir: string,
53+
auth: CliAuth,
5454
binPath: string,
5555
workspace: Workspace,
5656
writeEmitter: vscode.EventEmitter<string>,
@@ -65,7 +65,7 @@ export async function startWorkspaceIfStoppedOrFailed(
6565

6666
return new Promise((resolve, reject) => {
6767
const startArgs = [
68-
...getGlobalFlags(vscode.workspace.getConfiguration(), globalConfigDir),
68+
...getGlobalFlags(vscode.workspace.getConfiguration(), auth),
6969
"start",
7070
"--yes",
7171
createWorkspaceIdentifier(workspace),

0 commit comments

Comments
 (0)