From dab94a33a12d2676945f2a2bb2daba7897c0dfc8 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 10:46:35 -0500 Subject: [PATCH 1/5] fix: add non-null assertions for route params in authorization routes Hono's `c.req.param()` returns `string | undefined` but these params are always present because they're defined in the route path pattern. --- src/emulate/workos/routes/authorization-org-roles.ts | 8 ++++---- src/emulate/workos/routes/authorization-roles.ts | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index 1fd8bafa..cd71f978 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), + 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') }), + 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 3db6f85c..9feecd41 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 }), From 48f46807aad1b4d5efa95b5124c942ea5d2b761b Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 10:46:48 -0500 Subject: [PATCH 2/5] fix: --api-key flag takes precedence over WORKOS_API_KEY env var Previously the env var always won, making `--api-key` and `env switch` silently ineffective when WORKOS_API_KEY was set in the shell. Now the precedence follows standard CLI conventions: flag > env var > stored config. Also warns on `env switch` when WORKOS_API_KEY is set, since the env var will still override the stored environment key for commands that don't pass --api-key. --- src/commands/env.spec.ts | 49 ++++++++++++++++++++++++++++++++++++++++ src/commands/env.ts | 5 +++- src/lib/api-key.spec.ts | 9 ++++---- src/lib/api-key.ts | 8 +++---- src/utils/output.ts | 24 ++++++++++++++++++-- 5 files changed, 84 insertions(+), 11 deletions(-) diff --git a/src/commands/env.spec.ts b/src/commands/env.spec.ts index c04c2114..0cc0501c 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,36 @@ 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 () => { + delete process.env.WORKOS_API_KEY; + const stderrOutput: string[] = []; + vi.spyOn(console, 'error').mockImplementation((...args: unknown[]) => { + stderrOutput.push(args.map(String).join(' ')); + }); + await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); + await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); + await runEnvSwitch('sandbox'); + expect(stderrOutput).toHaveLength(0); + }); }); describe('runEnvList', () => { @@ -208,6 +239,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 aad62e0b..2ac01e6d 100644 --- a/src/commands/env.ts +++ b/src/commands/env.ts @@ -172,7 +172,10 @@ 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/lib/api-key.spec.ts b/src/lib/api-key.spec.ts index 0215a611..9d82ddbf 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 309aff62..6d884c62 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 f351739c..8a392f4c 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)); + } + } } } @@ -71,6 +83,14 @@ export function outputError(error: { code: string; message: string; details?: un } } +export function outputWarning(warning: { code: string; message: string }): void { + if (currentMode === 'json') { + console.error(JSON.stringify({ warning })); + } else { + console.error(chalk.yellow(warning.message)); + } +} + /** Write tabular data — chalk table in human mode, JSON array in json mode. */ export function outputTable(columns: TableColumn[], rows: string[][], rawData?: unknown[]): void { if (currentMode === 'json') { From 9d173a20450e98d2e82f99109b98f18eb7c462fb Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 11:04:48 -0500 Subject: [PATCH 3/5] chore: formatting --- src/commands/env.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/env.ts b/src/commands/env.ts index 2ac01e6d..ef26f361 100644 --- a/src/commands/env.ts +++ b/src/commands/env.ts @@ -173,7 +173,13 @@ export async function runEnvSwitch(name?: string): Promise { const env = config.environments[name]; 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." }] + ? [ + { + 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 }); } From 5b8a6ff9ea9943a60ef6b696c7ca35215ef32b37 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 11:05:32 -0500 Subject: [PATCH 4/5] fix: address PR review feedback - Remove unused `outputWarning` export (warnings are embedded in `outputSuccess` response instead) - Add missing `!` assertion on `orgId` in `listFilter` for consistency - Restore `WORKOS_API_KEY` env var in no-warning test to prevent leakage --- src/commands/env.spec.ts | 14 ++++++++++---- .../workos/routes/authorization-org-roles.ts | 2 +- src/utils/output.ts | 8 -------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/commands/env.spec.ts b/src/commands/env.spec.ts index 0cc0501c..978a67a9 100644 --- a/src/commands/env.spec.ts +++ b/src/commands/env.spec.ts @@ -169,15 +169,21 @@ describe('env commands', () => { }); 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(' ')); }); - await runEnvAdd({ name: 'prod', apiKey: 'sk_live_abc' }); - await runEnvAdd({ name: 'sandbox', apiKey: 'sk_test_abc' }); - await runEnvSwitch('sandbox'); - expect(stderrOutput).toHaveLength(0); + 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; + } }); }); diff --git a/src/emulate/workos/routes/authorization-org-roles.ts b/src/emulate/workos/routes/authorization-org-roles.ts index cd71f978..f09c2d5d 100644 --- a/src/emulate/workos/routes/authorization-org-roles.ts +++ b/src/emulate/workos/routes/authorization-org-roles.ts @@ -46,7 +46,7 @@ export function authorizationOrgRoleRoutes(ctx: RouteContext): void { 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', + 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) => { diff --git a/src/utils/output.ts b/src/utils/output.ts index 8a392f4c..fcd58ce7 100644 --- a/src/utils/output.ts +++ b/src/utils/output.ts @@ -83,14 +83,6 @@ export function outputError(error: { code: string; message: string; details?: un } } -export function outputWarning(warning: { code: string; message: string }): void { - if (currentMode === 'json') { - console.error(JSON.stringify({ warning })); - } else { - console.error(chalk.yellow(warning.message)); - } -} - /** Write tabular data — chalk table in human mode, JSON array in json mode. */ export function outputTable(columns: TableColumn[], rows: string[][], rawData?: unknown[]): void { if (currentMode === 'json') { From 7acaa3eae08df03831f8e6cce0762a616cb5ce50 Mon Sep 17 00:00:00 2001 From: Nick Nisi Date: Thu, 30 Apr 2026 11:17:30 -0500 Subject: [PATCH 5/5] docs: update API key precedence order in README Reflects the new flag > env var > stored config resolution order. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c762d013..77257b5a 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