Secure token storage, VS Code SecretStorage API, credential management, environment variable migration
Expert in secure credential storage for VS Code extensions using SecretStorage API, token lifecycle management, and security best practices.
| Method | Purpose | Returns |
|---|---|---|
secrets.store(key, value) | Save encrypted credential | Promise<void> |
secrets.get(key) | Retrieve credential | Promise<string | undefined> |
secrets.delete(key) | Remove credential | Promise<void> |
secrets.onDidChange | Listen for changes | Event<SecretStorageChangeEvent> |
| Platform | Storage Backend | Encryption |
|---|---|---|
| Windows | Windows Credential Manager | DPAPI (Data Protection API) |
| macOS | Keychain | Keychain Services |
| Linux | Secret Service API (libsecret) | OS keyring (GNOME/KDE) |
interface TokenConfig {
key: string; // SecretStorage key (namespaced)
displayName: string; // User-facing name
description: string; // Purpose explanation
getUrl?: string; // Where to obtain token
placeholder?: string; // Example format
envVar?: string; // Env var for migration
}
| Component | Responsibility |
|---|---|
| SecretStorage | OS-level encrypted storage (provided by VS Code) |
| Token Cache | In-memory Map for synchronous access |
| Config Registry | Token metadata (names, URLs, migration sources) |
| Migration Service | One-time env var → SecretStorage transfer |
| UI Service | Quick pick, input box, warning prompts |
| Practice | Implementation |
|---|---|
| Namespace keys | Use extension.namespace.tokenName format |
| Never log tokens | Use [REDACTED] in console output |
| Validate input | Check format before storage (regex patterns) |
| Non-destructive migration | Keep env vars as fallback |
| Clear on error | Don't cache failed retrievals |
| Use placeholders | Show format without real credentials |
| Password input | Set password: true on input boxes |
graph TD
A[Feature Requires Token] --> B{Check Cache}
B -->|Hit| C[Return Token]
B -->|Miss| D{Check SecretStorage}
D -->|Found| E[Cache + Return]
D -->|Not Found| F{Check Env Var}
F -->|Found| G[Migrate + Cache + Return]
F -->|Not Found| H[Prompt User]
H --> I{User Enters Token}
I -->|Success| J[Store + Cache + Return]
I -->|Cancel| K[Return Null]
SecretStorage is secure but inaccessible to external tools (CLI, PowerShell scripts, CI/CD). Solution: bidirectional flow.
graph LR
subgraph "Import (Migration)"
ENV1[.env file] -->|alex.migrateEnvSecrets| SS1[SecretStorage]
end
subgraph "Export (External Access)"
SS2[SecretStorage] -->|alex.exportSecretsToEnv| ENV2[.env file]
end
SS1 -.->|Same storage| SS2
| Direction | Command | Use Case |
|---|---|---|
| Import | Alex: Migrate .env to Secrets | Secure existing plaintext tokens |
| Export | Alex: Export Secrets to .env | Enable external tool access |
| Phase | Action | Safety Measure |
|---|---|---|
| Detection | Check for env var AND empty SecretStorage | Only migrate if both conditions met |
| Copy | secretStorage.store(key, process.env.VAR) | Non-destructive (env var remains) |
| Cache | Update in-memory cache | Avoid redundant SecretStorage reads |
| Fallback | If SecretStorage fails, use env var | Backward compatibility maintained |
| Logging | Console log migration success | User visibility without exposing tokens |
1. Feature triggered without token
↓
2. Warning message with 3 options:
- "Configure API Key" → Opens token manager
- "Get API Key" → Opens service URL in browser
- "Continue Anyway" → Proceeds (may fail)
↓
3. Token manager quick pick:
- List all tokens with status icons
- ✅ Configured | ❌ Not Configured
↓
4. Individual token prompt:
- Input box with password masking
- Placeholder showing format
- Validation before storage
↓
5. Confirmation:
- Success message
- Return to feature workflow
const result = await vscode.window.showWarningMessage(
`${SERVICE} API Key not configured. Set your API key to use ${FEATURE}.`,
"Configure API Key",
"Get API Key",
"Continue Anyway"
);
if (result === "Configure API Key") {
vscode.commands.executeCommand("extension.manageSecrets");
return;
}
if (result === "Get API Key") {
vscode.env.openExternal(vscode.Uri.parse(GET_URL));
return;
}
// Continue Anyway falls through
const TOKEN_CONFIGS: Record<string, TokenConfig> = {
SERVICE_TOKEN: {
key: 'extension.secrets.serviceToken',
displayName: 'Service API Token',
description: 'API token for external service integration',
getUrl: 'https://service.example.com/account/tokens',
placeholder: 'svc_xxxxxxxxxxxxxxxxxxxx',
envVar: 'SERVICE_API_TOKEN',
},
// Add more tokens as needed
};
Many VS Code APIs are synchronous, but SecretStorage is async. Solution:
// Module-level cache
const tokenCache: Map<string, string | null> = new Map();
// Async init (on activation)
async function initSecretsManager(context: vscode.ExtensionContext) {
secretStorage = context.secrets;
// Pre-load all tokens into cache
for (const config of Object.values(TOKEN_CONFIGS)) {
const token = await secretStorage.get(config.key);
tokenCache.set(config.key, token || null);
}
}
// Sync getter (safe after init)
function getToken(tokenName: string): string | null {
return tokenCache.get(tokenName) ?? null;
}
| Pitfall | Solution |
|---|---|
Calling SecretStorage before activate() | Initialize in activate(), check null |
| Async/sync mismatch | Use cache pattern for sync access |
| Logging actual tokens | Use console.log(\Migrated ${name}`)` without value |
| Overwriting user tokens | Check storage before migration |
| Hard-coded API keys | Always use SecretStorage |
| No fallback for missing tokens | Warn + offer config, don't crash |
| Platform-specific code | VS Code SecretStorage abstracts OS differences |
Alex can automatically detect secrets in .env files and offer secure migration:
Detection Pattern:
// Scan workspace for .env files (excludes .env.example, .env.template)
const envFiles = await vscode.workspace.findFiles('**/.env*', '**/node_modules/**');
// Parse for secret patterns
const secretKeywords = [
'API_KEY', 'API_TOKEN', 'SECRET', 'PASSWORD', 'PASS',
'TOKEN', 'AUTH', 'CREDENTIAL', 'PRIVATE_KEY',
'ACCESS_KEY', 'SECRET_KEY', 'CLIENT_SECRET'
];
// Match: KEY_NAME=value (handles quotes, spaces, comments)
const envPattern = /^\s*([A-Z_][A-Z0-9_]*)\s*=\s*([^#\n]+)/i;
Migration Workflow:
.env files in workspaceUser Commands:
Alex: Detect & Migrate .env Secrets - Scan workspace for .env filesAlex: Export Secrets to .env - Write SecretStorage tokens to .env for external tool accessMigration UI Flow:
🔍 Found 3 potential secret(s) in .env files:
✅ 2 recognized (can auto-migrate)
⚠️ 1 custom (requires manual setup)
[Review Secrets] [Auto-Migrate Recognized] [Cancel]
Code Migration Guide: After migration, users must update their code:
context.secrets APISecurity Benefits:
.env filesVS Code SecretStorage is inaccessible to PowerShell scripts, CLI tools, and CI/CD pipelines. The export command bridges this gap.
Why Export is Needed:
brain-qa.ps1) can't access SecretStorageExport Implementation:
async function exportSecretsToEnv(): Promise<void> {
const workspaceFolder = vscode.workspace.workspaceFolders?.[0];
if (!workspaceFolder) return;
const envPath = path.join(workspaceFolder.uri.fsPath, '.env');
const secrets: string[] = [];
// Collect cached secrets
for (const [key, value] of tokenCache.entries()) {
if (value) {
// Find env var name from config
const config = Object.values(TOKEN_CONFIGS)
.find(c => c.key === key);
if (config?.envVar) {
secrets.push(`${config.envVar}=${value}`);
}
}
}
if (secrets.length === 0) {
vscode.window.showWarningMessage('No secrets to export');
return;
}
// Read existing .env, replace Alex section
let content = '';
if (fs.existsSync(envPath)) {
content = fs.readFileSync(envPath, 'utf-8');
// Remove existing Alex section
content = content.replace(
/\n?# Alex Secrets Export[\s\S]*?(?=\n#|$)/g, ''
).trim();
}
// Append new section
const section = `\n\n# Alex Secrets Export (auto-generated)\n${secrets.join('\n')}`;
fs.writeFileSync(envPath, content + section, 'utf-8');
vscode.window.showInformationMessage(
`Exported ${secrets.length} secret(s) to .env`
);
}
PowerShell Script Usage:
# Source the .env file in PowerShell
if (Test-Path .env) {
Get-Content .env | ForEach-Object {
if ($_ -match '^([^#=]+)=(.*)$') {
[Environment]::SetEnvironmentVariable($matches[1].Trim(), $matches[2].Trim())
}
}
}
# Now $env:REPLICATE_API_TOKEN is available
Security Considerations:
.gitignoresecretsManager.ts serviceinitSecretsManager(context) - Set up storage + cachegetToken(name) - Synchronous retrieval with fallbacksetToken(name, value) - Store + update cachedeleteToken(name) - Remove + clear cachemigrateSecretsFromEnvironment() - One-time migrationinitSecretsManager() in activate()getToken() instead of env vars