Skip to content

Commit 80f0c0a

Browse files
authored
feat: store session tokens in the OS keyring (#808)
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, or older CLIs. Key changes: - Harden file fallback with mode 0o600 - 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) - Add KeyringStore wrapping @napi-rs/keyring with the CLI's credential format (JSON map keyed by host, base64 on macOS, raw bytes on Windows)
1 parent 653e878 commit 80f0c0a

24 files changed

+1281
-155
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
## Unreleased
44

5+
### Added
6+
7+
- Session tokens are now stored in the OS keyring (Keychain on macOS, Credential Manager on
8+
Windows) instead of plaintext files, when using CLI >= 2.29.0. Falls back to file storage on
9+
Linux, older CLIs, or if the keyring write fails. Controlled via the `coder.useKeyring` setting.
10+
511
## [v1.13.0](https://github.com/coder/vscode-coder/releases/tag/v1.13.0) 2026-03-03
612

713
### Added

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
@@ -154,7 +154,7 @@ export default defineConfig(
154154

155155
// Build config - ESM with Node globals
156156
{
157-
files: ["esbuild.mjs"],
157+
files: ["esbuild.mjs", "scripts/*.mjs"],
158158
languageOptions: {
159159
globals: {
160160
...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"
@@ -156,6 +156,11 @@
156156
"type": "string"
157157
}
158158
},
159+
"coder.useKeyring": {
160+
"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.",
161+
"type": "boolean",
162+
"default": true
163+
},
159164
"coder.httpClientLogLevel": {
160165
"markdownDescription": "Controls the verbosity of HTTP client logging. This affects what details are logged for each HTTP request and response.",
161166
"type": "string",
@@ -463,6 +468,7 @@
463468
"word-wrap": "1.2.5"
464469
},
465470
"dependencies": {
471+
"@napi-rs/keyring": "^1.2.0",
466472
"@peculiar/x509": "^1.14.3",
467473
"@repo/shared": "workspace:*",
468474
"axios": "1.13.6",

pnpm-lock.yaml

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

pnpm-workspace.yaml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,16 @@ onlyBuiltDependencies:
2727
- keytar
2828
- unrs-resolver
2929
- utf-8-validate
30+
31+
# Install @napi-rs/keyring native binaries for macOS and Windows so they're
32+
# available when building the universal VSIX (even on Linux CI).
33+
# Only macOS and Windows use the keyring; Linux falls back to file storage.
34+
supportedArchitectures:
35+
os:
36+
- current
37+
- darwin
38+
- win32
39+
cpu:
40+
- current
41+
- x64
42+
- arm64

scripts/vendor-keyring.mjs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 {
9+
cpSync,
10+
existsSync,
11+
mkdirSync,
12+
readdirSync,
13+
realpathSync,
14+
rmSync,
15+
} from "node:fs";
16+
import { join, resolve } from "node:path";
17+
18+
const keyringPkg = resolve("node_modules/@napi-rs/keyring");
19+
const outputDir = resolve("dist/node_modules/@napi-rs/keyring");
20+
21+
if (!existsSync(keyringPkg)) {
22+
console.error("@napi-rs/keyring not found — run pnpm install first");
23+
process.exit(1);
24+
}
25+
26+
// Copy the JS wrapper package (resolving pnpm symlinks)
27+
const resolvedPkg = realpathSync(keyringPkg);
28+
rmSync(outputDir, { recursive: true, force: true });
29+
mkdirSync(outputDir, { recursive: true });
30+
cpSync(resolvedPkg, outputDir, { recursive: true });
31+
32+
// Native binary packages live as siblings of the resolved keyring package in
33+
// pnpm's content-addressable store (they aren't hoisted to node_modules).
34+
const siblingsDir = resolve(resolvedPkg, "..");
35+
const nativePackages = [
36+
"keyring-darwin-arm64",
37+
"keyring-darwin-x64",
38+
"keyring-win32-arm64-msvc",
39+
"keyring-win32-x64-msvc",
40+
];
41+
42+
for (const pkg of nativePackages) {
43+
const pkgDir = join(siblingsDir, pkg);
44+
if (!existsSync(pkgDir)) {
45+
console.error(
46+
`Missing native package: ${pkg}\n` +
47+
"Ensure supportedArchitectures in pnpm-workspace.yaml includes all target platforms.",
48+
);
49+
process.exit(1);
50+
}
51+
const nodeFile = readdirSync(pkgDir).find((f) => f.endsWith(".node"));
52+
if (!nodeFile) {
53+
console.error(`No .node binary found in ${pkg}`);
54+
process.exit(1);
55+
}
56+
cpSync(join(pkgDir, nodeFile), join(outputDir, nodeFile));
57+
}
58+
59+
console.log(
60+
`Vendored @napi-rs/keyring with ${nativePackages.length} platform binaries into dist/`,
61+
);

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)