Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <resource> <action> [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 <resource> <action> [args] [--options]`. API keys resolve via: `--api-key` flag → `WORKOS_API_KEY` env var → active environment's stored key.

#### organization

Expand Down
55 changes: 55 additions & 0 deletions src/commands/env.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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;
}
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
});

describe('runEnvList', () => {
Expand Down Expand Up @@ -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' });
Expand Down
11 changes: 10 additions & 1 deletion src/commands/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,16 @@ export async function runEnvSwitch(name?: string): Promise<void> {
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 });
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

export async function runEnvList(): Promise<void> {
Expand Down
10 changes: 5 additions & 5 deletions src/emulate/workos/routes/authorization-org-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
},
});
Expand Down
2 changes: 1 addition & 1 deletion src/emulate/workos/routes/authorization-roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }),
Expand Down
9 changes: 5 additions & 4 deletions src/lib/api-key.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
8 changes: 4 additions & 4 deletions src/lib/api-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

Expand All @@ -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;

Expand Down
16 changes: 14 additions & 2 deletions src/utils/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown> = { 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));
}
}
}
}

Expand Down
Loading