|
1 | | -import { describe, it, expect, vi, beforeEach } from 'vitest'; |
| 1 | +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; |
2 | 2 |
|
3 | 3 | const { mockQuery, mockConfig } = vi.hoisted(() => ({ |
4 | 4 | mockQuery: vi.fn(), |
@@ -61,13 +61,22 @@ vi.mock('./token-refresh.js', () => ({ |
61 | 61 |
|
62 | 62 | vi.mock('./credential-proxy.js', () => ({ |
63 | 63 | startCredentialProxy: vi.fn(), |
| 64 | + startClaimTokenProxy: vi.fn(), |
| 65 | +})); |
| 66 | + |
| 67 | +vi.mock('./config-store.js', () => ({ |
| 68 | + getActiveEnvironment: vi.fn(() => null), |
| 69 | + isUnclaimedEnvironment: vi.fn(() => false), |
64 | 70 | })); |
65 | 71 |
|
66 | 72 | vi.mock('../utils/urls.js', () => ({ |
67 | 73 | getLlmGatewayUrlFromHost: vi.fn(() => 'http://localhost:8000'), |
68 | 74 | })); |
69 | 75 |
|
70 | | -import { runAgent, AgentErrorType } from './agent-interface.js'; |
| 76 | +import { runAgent, AgentErrorType, initializeAgent } from './agent-interface.js'; |
| 77 | +import { startCredentialProxy, startClaimTokenProxy } from './credential-proxy.js'; |
| 78 | +import { getActiveEnvironment, isUnclaimedEnvironment } from './config-store.js'; |
| 79 | +import { hasCredentials, getCredentials } from './credentials.js'; |
71 | 80 | import { InstallerEventEmitter } from './events.js'; |
72 | 81 | import type { InstallerOptions } from '../utils/types.js'; |
73 | 82 |
|
@@ -364,3 +373,114 @@ describe('service unavailability handling', () => { |
364 | 373 | expect(validateAndFormat).not.toHaveBeenCalled(); |
365 | 374 | }); |
366 | 375 | }); |
| 376 | + |
| 377 | +describe('initializeAgent sdkEnv auth', () => { |
| 378 | + const PROXY_PLACEHOLDER_TOKEN = 'workos-cli-proxy-placeholder'; |
| 379 | + const originalAnthropicApiKey = process.env.ANTHROPIC_API_KEY; |
| 380 | + const originalAnthropicAuthToken = process.env.ANTHROPIC_AUTH_TOKEN; |
| 381 | + |
| 382 | + beforeEach(() => { |
| 383 | + vi.mocked(startCredentialProxy).mockReset(); |
| 384 | + vi.mocked(startClaimTokenProxy).mockReset(); |
| 385 | + vi.mocked(getActiveEnvironment).mockReset().mockReturnValue(null); |
| 386 | + vi.mocked(isUnclaimedEnvironment).mockReset().mockReturnValue(false); |
| 387 | + vi.mocked(hasCredentials).mockReset().mockReturnValue(false); |
| 388 | + vi.mocked(getCredentials).mockReset().mockReturnValue(null); |
| 389 | + |
| 390 | + // Simulate a user shell that has their own Anthropic key sitting in the |
| 391 | + // environment. The SDK must NOT forward this to the WorkOS gateway. |
| 392 | + process.env.ANTHROPIC_API_KEY = 'sk-ant-user-personal-key'; |
| 393 | + delete process.env.ANTHROPIC_AUTH_TOKEN; |
| 394 | + }); |
| 395 | + |
| 396 | + afterEach(() => { |
| 397 | + if (originalAnthropicApiKey === undefined) { |
| 398 | + delete process.env.ANTHROPIC_API_KEY; |
| 399 | + } else { |
| 400 | + process.env.ANTHROPIC_API_KEY = originalAnthropicApiKey; |
| 401 | + } |
| 402 | + if (originalAnthropicAuthToken === undefined) { |
| 403 | + delete process.env.ANTHROPIC_AUTH_TOKEN; |
| 404 | + } else { |
| 405 | + process.env.ANTHROPIC_AUTH_TOKEN = originalAnthropicAuthToken; |
| 406 | + } |
| 407 | + }); |
| 408 | + |
| 409 | + function makeAgentConfigForInit() { |
| 410 | + return { |
| 411 | + workingDirectory: '/tmp/test', |
| 412 | + installDir: '/tmp/test', |
| 413 | + }; |
| 414 | + } |
| 415 | + |
| 416 | + it('seeds placeholder auth token on the credential proxy path', async () => { |
| 417 | + vi.mocked(hasCredentials).mockReturnValue(true); |
| 418 | + vi.mocked(getCredentials).mockReturnValue({ |
| 419 | + accessToken: 'real-workos-token', |
| 420 | + refreshToken: 'refresh-token', |
| 421 | + expiresAt: Date.now() + 60_000, |
| 422 | + }); |
| 423 | + vi.mocked(startCredentialProxy).mockResolvedValue({ |
| 424 | + port: 12345, |
| 425 | + url: 'http://127.0.0.1:12345', |
| 426 | + stop: vi.fn(async () => {}), |
| 427 | + }); |
| 428 | + |
| 429 | + const result = await initializeAgent(makeAgentConfigForInit(), makeOptions({ skipAuth: false, local: false })); |
| 430 | + |
| 431 | + // The SDK runs a local auth-source check at startup and exits with |
| 432 | + // "Not logged in" if nothing is present. A placeholder token prevents |
| 433 | + // that false-positive; the proxy overwrites Authorization upstream. |
| 434 | + expect(result.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(PROXY_PLACEHOLDER_TOKEN); |
| 435 | + // User's personal Anthropic key must not leak through to the gateway. |
| 436 | + expect(result.sdkEnv.ANTHROPIC_API_KEY).toBeUndefined(); |
| 437 | + expect(result.sdkEnv.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:12345'); |
| 438 | + }); |
| 439 | + |
| 440 | + it('seeds placeholder auth token on the claim-token proxy path', async () => { |
| 441 | + vi.mocked(getActiveEnvironment).mockReturnValue({ |
| 442 | + apiKey: 'sk_test_x', |
| 443 | + clientId: 'client_x', |
| 444 | + claimToken: 'claim_xyz', |
| 445 | + } as unknown as ReturnType<typeof getActiveEnvironment>); |
| 446 | + vi.mocked(isUnclaimedEnvironment).mockReturnValue(true); |
| 447 | + vi.mocked(startClaimTokenProxy).mockResolvedValue({ |
| 448 | + port: 23456, |
| 449 | + url: 'http://127.0.0.1:23456', |
| 450 | + stop: vi.fn(async () => {}), |
| 451 | + }); |
| 452 | + |
| 453 | + const result = await initializeAgent(makeAgentConfigForInit(), makeOptions({ skipAuth: false, local: false })); |
| 454 | + |
| 455 | + expect(result.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(PROXY_PLACEHOLDER_TOKEN); |
| 456 | + expect(result.sdkEnv.ANTHROPIC_API_KEY).toBeUndefined(); |
| 457 | + expect(result.sdkEnv.ANTHROPIC_BASE_URL).toBe('http://127.0.0.1:23456'); |
| 458 | + }); |
| 459 | + |
| 460 | + it('seeds placeholder auth token in skip-auth mode', async () => { |
| 461 | + const result = await initializeAgent(makeAgentConfigForInit(), makeOptions({ skipAuth: true, local: false })); |
| 462 | + |
| 463 | + expect(result.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(PROXY_PLACEHOLDER_TOKEN); |
| 464 | + expect(result.sdkEnv.ANTHROPIC_API_KEY).toBeUndefined(); |
| 465 | + }); |
| 466 | + |
| 467 | + it('seeds placeholder auth token in local mode', async () => { |
| 468 | + const result = await initializeAgent(makeAgentConfigForInit(), makeOptions({ skipAuth: false, local: true })); |
| 469 | + |
| 470 | + expect(result.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBe(PROXY_PLACEHOLDER_TOKEN); |
| 471 | + expect(result.sdkEnv.ANTHROPIC_API_KEY).toBeUndefined(); |
| 472 | + }); |
| 473 | + |
| 474 | + it('preserves ANTHROPIC_API_KEY in direct mode', async () => { |
| 475 | + const result = await initializeAgent( |
| 476 | + makeAgentConfigForInit(), |
| 477 | + makeOptions({ direct: true, skipAuth: false, local: false }), |
| 478 | + ); |
| 479 | + |
| 480 | + // Direct mode talks to api.anthropic.com using the user's own key; |
| 481 | + // the placeholder bearer must NOT be set here. |
| 482 | + expect(result.sdkEnv.ANTHROPIC_API_KEY).toBe('sk-ant-user-personal-key'); |
| 483 | + expect(result.sdkEnv.ANTHROPIC_AUTH_TOKEN).toBeUndefined(); |
| 484 | + expect(result.sdkEnv.ANTHROPIC_BASE_URL).toBeUndefined(); |
| 485 | + }); |
| 486 | +}); |
0 commit comments