Skip to content

Commit 9e02ee8

Browse files
authored
Merge pull request #1402 from vybestack/issue1351_1352
feat: KeyringTokenStore replaces plaintext OAuth token storage (Fixes #1351, Fixes #1352)
2 parents 4fc0562 + 433139b commit 9e02ee8

File tree

65 files changed

+15894
-1254
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+15894
-1254
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ jobs:
374374
# Test: Node (Linux and macOS - always run)
375375
#
376376
test:
377-
name: 'Test'
377+
name: 'Test (${{ matrix.os }}, ${{ matrix.secure-store-mode }})'
378378
runs-on: '${{ matrix.os }}'
379379
needs:
380380
- 'lint'
@@ -393,6 +393,9 @@ jobs:
393393
- 'macos-latest'
394394
node-version:
395395
- '24.x'
396+
secure-store-mode:
397+
- 'keyring'
398+
- 'fallback'
396399
steps:
397400
- name: 'Checkout'
398401
uses: 'actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8' # ratchet:actions/checkout@v5
@@ -464,6 +467,8 @@ jobs:
464467
CI: true
465468
# Allow OpenAI integration tests to exceed Vitest's 5s default timeout (issue #338)
466469
VITEST_TEST_TIMEOUT: 15000
470+
# Force SecureStore to use encrypted-file fallback instead of keyring (R15.2)
471+
LLXPRT_SECURE_STORE_FORCE_FALLBACK: ${{ matrix.secure-store-mode == 'fallback' && 'true' || '' }}
467472
run: 'npm run test'
468473

469474
- name: 'Run script harness tests (macOS)'
@@ -499,15 +504,15 @@ jobs:
499504
${{ always() && (github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository) }}
500505
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
501506
with:
502-
name: 'test-results-fork-${{ matrix.node-version }}-${{ matrix.os }}'
507+
name: 'test-results-fork-${{ matrix.node-version }}-${{ matrix.os }}-${{ matrix.secure-store-mode }}'
503508
path: 'packages/*/junit.xml'
504509

505510
- name: 'Upload coverage reports'
506511
if: |-
507512
${{ always() }}
508513
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # ratchet:actions/upload-artifact@v4
509514
with:
510-
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
515+
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}-${{ matrix.secure-store-mode }}'
511516
path: 'packages/*/coverage'
512517

513518
post_coverage_comment:
@@ -534,7 +539,7 @@ jobs:
534539
- name: 'Download coverage reports artifact'
535540
uses: 'actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0' # ratchet:actions/download-artifact@v5
536541
with:
537-
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}'
542+
name: 'coverage-reports-${{ matrix.node-version }}-${{ matrix.os }}-keyring'
538543
path: 'coverage_artifact' # Download to a specific directory
539544

540545
- name: 'Post Coverage Comment using Composite Action'

packages/cli/src/auth/__tests__/codex-oauth-provider.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
88
import { CodexOAuthProvider } from '../codex-oauth-provider.js';
99
import {
10-
MultiProviderTokenStore,
10+
KeyringTokenStore,
1111
CodexOAuthTokenSchema,
1212
} from '@vybestack/llxprt-code-core';
1313
import type { TokenStore } from '@vybestack/llxprt-code-core';
@@ -36,7 +36,7 @@ describe('CodexOAuthProvider', () => {
3636
process.env.HOME = tempDir;
3737

3838
// tokenStore constructor uses homedir() which reads from process.env.HOME
39-
tokenStore = new MultiProviderTokenStore();
39+
tokenStore = new KeyringTokenStore();
4040
provider = new CodexOAuthProvider(tokenStore);
4141
});
4242

packages/cli/src/auth/codex-oauth-provider.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ export class CodexOAuthProvider implements OAuthProvider {
302302
`[FLOW] Token received: access_token=${token.access_token.substring(0, 10)}..., account_id=${token.account_id?.substring(0, 8) ?? 'MISSING'}..., expiry=${token.expiry}`,
303303
);
304304

305-
// Save to MultiProviderTokenStore location (~/.llxprt/oauth/codex.json)
305+
// Save to KeyringTokenStore (keyring or encrypted fallback)
306306
this.logger.debug(() => '[FLOW] Saving token to tokenStore...');
307307
await this.tokenStore.saveToken('codex', token);
308308
this.logger.debug(() => '[FLOW] Token saved to tokenStore');
@@ -436,7 +436,7 @@ export class CodexOAuthProvider implements OAuthProvider {
436436
this.logger.debug(() => '[FLOW] getToken() called');
437437
await this.ensureInitialized();
438438

439-
// Get token from ~/.llxprt/oauth/codex.json
439+
// Get token from KeyringTokenStore (keyring or encrypted fallback)
440440
this.logger.debug(() => '[FLOW] Reading token from tokenStore...');
441441
const token = await this.tokenStore.getToken('codex');
442442

packages/cli/src/auth/oauth-manager-initialization.spec.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import { describe, it, expect, beforeEach, vi } from 'vitest';
88
import { OAuthManager } from './oauth-manager.js';
9-
import { MultiProviderTokenStore } from './types.js';
9+
import { KeyringTokenStore } from './types.js';
1010
import { GeminiOAuthProvider } from './gemini-oauth-provider.js';
1111
import { QwenOAuthProvider } from './qwen-oauth-provider.js';
1212
import { AnthropicOAuthProvider } from './anthropic-oauth-provider.js';
@@ -28,12 +28,12 @@ vi.mock('node:fs', async (importOriginal) => {
2828
const mockFs = vi.mocked(fs);
2929

3030
describe('OAuth Provider Premature Initialization', () => {
31-
let tokenStore: MultiProviderTokenStore;
31+
let tokenStore: KeyringTokenStore;
3232
let oauthManager: OAuthManager;
3333

3434
beforeEach(() => {
3535
vi.clearAllMocks();
36-
tokenStore = new MultiProviderTokenStore();
36+
tokenStore = new KeyringTokenStore();
3737
oauthManager = new OAuthManager(tokenStore);
3838

3939
// Mock the OAuth credentials file to not exist

packages/cli/src/auth/oauth-manager.refresh-race.spec.ts

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,37 @@ import type { OAuthToken, TokenStore } from './types.js';
1515
import { promises as fs } from 'fs';
1616
import { join } from 'path';
1717
import { tmpdir } from 'os';
18-
import { MultiProviderTokenStore } from '@vybestack/llxprt-code-core';
18+
import {
19+
KeyringTokenStore,
20+
SecureStore,
21+
type KeyringAdapter,
22+
} from '@vybestack/llxprt-code-core';
23+
24+
function createMockKeyring(): KeyringAdapter & { store: Map<string, string> } {
25+
const store = new Map<string, string>();
26+
return {
27+
store,
28+
getPassword: async (service: string, account: string) =>
29+
store.get(`${service}:${account}`) ?? null,
30+
setPassword: async (service: string, account: string, password: string) => {
31+
store.set(`${service}:${account}`, password);
32+
},
33+
deletePassword: async (service: string, account: string) =>
34+
store.delete(`${service}:${account}`),
35+
findCredentials: async (service: string) => {
36+
const results: Array<{ account: string; password: string }> = [];
37+
for (const [key, value] of store.entries()) {
38+
if (key.startsWith(`${service}:`)) {
39+
results.push({
40+
account: key.slice(service.length + 1),
41+
password: value,
42+
});
43+
}
44+
}
45+
return results;
46+
},
47+
};
48+
}
1949

2050
describe('OAuthManager - Token Refresh Race Condition (Issue #1159)', () => {
2151
let tempDir: string;
@@ -35,7 +65,12 @@ describe('OAuthManager - Token Refresh Race Condition (Issue #1159)', () => {
3565
beforeEach(async () => {
3666
// Create a temporary directory for testing
3767
tempDir = await fs.mkdtemp(join(tmpdir(), 'oauth-refresh-race-test-'));
38-
tokenStore = new MultiProviderTokenStore(tempDir);
68+
const secureStore = new SecureStore('llxprt-code-oauth', {
69+
fallbackDir: tempDir,
70+
fallbackPolicy: 'allow',
71+
keyringLoader: async () => createMockKeyring(),
72+
});
73+
tokenStore = new KeyringTokenStore({ secureStore });
3974
oauthManager = new OAuthManager(tokenStore);
4075

4176
// Reset refresh call counter

packages/cli/src/auth/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,4 @@ export type {
1111
TokenStore,
1212
} from '@vybestack/llxprt-code-core';
1313

14-
export { MultiProviderTokenStore } from '@vybestack/llxprt-code-core';
14+
export { KeyringTokenStore } from '@vybestack/llxprt-code-core';

0 commit comments

Comments
 (0)