diff --git a/README.md b/README.md index c762d01..77257b5 100644 --- a/README.md +++ b/README.md @@ -367,7 +367,7 @@ API keys are stored in the system keychain via `@napi-rs/keyring`, with a JSON f ### Resource Management -All resource commands follow the same pattern: `workos [args] [--options]`. API keys resolve via: `WORKOS_API_KEY` env var → `--api-key` flag → active environment's stored key. +All resource commands follow the same pattern: `workos [args] [--options]`. API keys resolve via: `--api-key` flag → `WORKOS_API_KEY` env var → active environment's stored key. #### organization diff --git a/src/commands/env.spec.ts b/src/commands/env.spec.ts index c04c211..978a67a 100644 --- a/src/commands/env.spec.ts +++ b/src/commands/env.spec.ts @@ -16,6 +16,7 @@ vi.mock('../utils/clack.js', () => ({ error: vi.fn(), info: vi.fn(), step: vi.fn(), + warn: vi.fn(), }, text: vi.fn(), select: vi.fn(), @@ -148,6 +149,42 @@ describe('env commands', () => { it('errors when no environments configured', async () => { await expect(runEnvSwitch('anything')).rejects.toThrow('process.exit'); }); + + it('warns when WORKOS_API_KEY env var is set', async () => { + const original = process.env.WORKOS_API_KEY; + process.env.WORKOS_API_KEY = 'sk_test_override'; + const stderrOutput: string[] = []; + vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + stderrOutput.push(args.map(String).join(' ')); + }); + try { + await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); + await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); + await runEnvSwitch('sandbox'); + expect(stderrOutput.some((s) => s.includes('WORKOS_API_KEY'))).toBe(true); + } finally { + if (original === undefined) delete process.env.WORKOS_API_KEY; + else process.env.WORKOS_API_KEY = original; + } + }); + + it('does not warn when WORKOS_API_KEY env var is not set', async () => { + const original = process.env.WORKOS_API_KEY; + delete process.env.WORKOS_API_KEY; + const stderrOutput: string[] = []; + vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + stderrOutput.push(args.map(String).join(' ')); + }); + try { + await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); + await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); + await runEnvSwitch('sandbox'); + expect(stderrOutput).toHaveLength(0); + } finally { + if (original === undefined) delete process.env.WORKOS_API_KEY; + else process.env.WORKOS_API_KEY = original; + } + }); }); describe('runEnvList', () => { @@ -208,6 +245,24 @@ describe('env commands', () => { expect(output.data.name).toBe('sandbox'); }); + it('runEnvSwitch includes warnings in JSON when WORKOS_API_KEY is set', async () => { + const original = process.env.WORKOS_API_KEY; + process.env.WORKOS_API_KEY = 'sk_test_override'; + try { + await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); + await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); + consoleOutput = []; + await runEnvSwitch('sandbox'); + const output = JSON.parse(consoleOutput[0]); + expect(output.status).toBe('ok'); + expect(output.warnings).toHaveLength(1); + expect(output.warnings[0].code).toBe('env_var_override'); + } finally { + if (original === undefined) delete process.env.WORKOS_API_KEY; + else process.env.WORKOS_API_KEY = original; + } + }); + it('runEnvList outputs JSON with data array', async () => { await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); diff --git a/src/commands/env.ts b/src/commands/env.ts index aad62e0..ef26f36 100644 --- a/src/commands/env.ts +++ b/src/commands/env.ts @@ -172,7 +172,16 @@ export async function runEnvSwitch(name?: string): Promise { saveConfig(config); const env = config.environments[name]; - outputSuccess('Switched environment', { name, type: env.type }); + const warnings = process.env.WORKOS_API_KEY + ? [ + { + code: 'env_var_override', + message: + "WORKOS_API_KEY is set in your shell. It will override this environment's stored key unless you pass --api-key.", + }, + ] + : undefined; + outputSuccess('Switched environment', { name, type: env.type }, { warnings }); } export async function runEnvList(): Promise { diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index 1fd8baf..f09c2d5 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -44,13 +44,13 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { registerRoleRoutes(ctx, { pathPrefix: prefix, roleType: 'OrganizationRole', - requireRole: (ws, c) => requireOrgRole(ws, c.req.param('orgId'), c.req.param('slug')), - findRole: (ws, c, slug) => findOrgRole(ws, c.req.param('orgId'), slug), - listFilter: (c) => (r) => r.organization_id === c.req.param('orgId') && r.type === 'OrganizationRole', - insertDefaults: (c) => ({ organization_id: c.req.param('orgId') }), + requireRole: (ws, c) => requireOrgRole(ws, c.req.param('orgId')!, c.req.param('slug')!), + findRole: (ws, c, slug) => findOrgRole(ws, c.req.param('orgId')!, slug), + listFilter: (c) => (r) => r.organization_id === c.req.param('orgId')! && r.type === 'OrganizationRole', + insertDefaults: (c) => ({ organization_id: c.req.param('orgId')! }), duplicateMessage: 'Role with this slug already exists in this organization', validateBeforeCreate: (ws, c) => { - const org = ws.organizations.get(c.req.param('orgId')); + const org = ws.organizations.get(c.req.param('orgId')!); if (!org) throw notFound('Organization'); }, }); diff --git a/src/emulate/workos/routes/authorization-roles.ts b/src/emulate/workos/routes/authorization-roles.ts index 3db6f85..9feecd4 100644 --- a/src/emulate/workos/routes/authorization-roles.ts +++ b/src/emulate/workos/routes/authorization-roles.ts @@ -5,7 +5,7 @@ export function authorizationRoleRoutes(ctx: RouteContext): void { registerRoleRoutes(ctx, { pathPrefix: '/authorization/roles', roleType: 'EnvironmentRole', - requireRole: (ws, c) => requireEnvRole(ws, c.req.param('slug')), + requireRole: (ws, c) => requireEnvRole(ws, c.req.param('slug')!), findRole: (ws, _c, slug) => findEnvRole(ws, slug), listFilter: () => (r) => r.type === 'EnvironmentRole', insertDefaults: () => ({ organization_id: null }), diff --git a/src/lib/api-key.spec.ts b/src/lib/api-key.spec.ts index 0215a61..9d82ddb 100644 --- a/src/lib/api-key.spec.ts +++ b/src/lib/api-key.spec.ts @@ -63,21 +63,22 @@ describe('api-key', () => { }); describe('resolveApiKey', () => { - it('returns WORKOS_API_KEY env var when set', () => { + it('returns --api-key flag over env var and stored key', () => { process.env.WORKOS_API_KEY = 'sk_env_var'; saveConfig({ activeEnvironment: 'prod', environments: { prod: { name: 'prod', type: 'production', apiKey: 'sk_stored' } }, }); - expect(resolveApiKey({ apiKey: 'sk_flag' })).toBe('sk_env_var'); + expect(resolveApiKey({ apiKey: 'sk_flag' })).toBe('sk_flag'); }); - it('returns --api-key flag when env var not set', () => { + it('returns WORKOS_API_KEY env var when no flag provided', () => { + process.env.WORKOS_API_KEY = 'sk_env_var'; saveConfig({ activeEnvironment: 'prod', environments: { prod: { name: 'prod', type: 'production', apiKey: 'sk_stored' } }, }); - expect(resolveApiKey({ apiKey: 'sk_flag' })).toBe('sk_flag'); + expect(resolveApiKey()).toBe('sk_env_var'); }); it('returns active environment API key when no env var or flag', () => { diff --git a/src/lib/api-key.ts b/src/lib/api-key.ts index 309aff6..6d884c6 100644 --- a/src/lib/api-key.ts +++ b/src/lib/api-key.ts @@ -2,8 +2,8 @@ * API key resolution for management commands. * * Priority chain: - * 1. WORKOS_API_KEY environment variable - * 2. --api-key flag + * 1. --api-key flag + * 2. WORKOS_API_KEY environment variable * 3. Active environment's stored API key */ @@ -17,11 +17,11 @@ export interface ApiKeyOptions { } export function resolveApiKey(options?: ApiKeyOptions): string { + if (options?.apiKey) return options.apiKey; + const envVar = process.env.WORKOS_API_KEY; if (envVar) return envVar; - if (options?.apiKey) return options.apiKey; - const activeEnv = getActiveEnvironment(); if (activeEnv?.apiKey) return activeEnv.apiKey; diff --git a/src/utils/output.ts b/src/utils/output.ts index f351739..fcd58ce 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -51,14 +51,26 @@ export function outputJson(data: unknown): void { } /** Write a success result — chalk in human mode, JSON in json mode. */ -export function outputSuccess(message: string, data?: object): void { +export function outputSuccess( + message: string, + data?: object, + options?: { warnings?: Array<{ code: string; message: string }> }, +): void { if (currentMode === 'json') { - console.log(JSON.stringify(data ? { status: 'ok', message, data } : { status: 'ok', message })); + const result: Record = { status: 'ok', message }; + if (data) result.data = data; + if (options?.warnings?.length) result.warnings = options.warnings; + console.log(JSON.stringify(result)); } else { console.log(chalk.green(message)); if (data) { console.log(JSON.stringify(data, null, 2)); } + if (options?.warnings?.length) { + for (const w of options.warnings) { + console.error(chalk.yellow(w.message)); + } + } } }