Skip to content

Commit 2a9adff

Browse files
committed
fix: tighten auth recovery guidance
1 parent f8a8a44 commit 2a9adff

12 files changed

Lines changed: 67 additions & 38 deletions

README.md

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ npm install -g workos
1313
workos install
1414
```
1515

16+
`npx workos@latest install` is recommended because it bypasses stale global shims and older shell-resolved binaries.
17+
If a global install reports `unknown command "install"`, run the npx command above or reinstall globally and clear your
18+
shell command cache.
19+
1620
## Features
1721

1822
- **15 Framework Support:** Next.js, React Router, TanStack Start, React SPA, Vanilla JS, SvelteKit, Node.js (Express), Python (Django), Ruby (Rails), Go, .NET (ASP.NET Core), Kotlin (Spring Boot), Elixir (Phoenix), PHP (Laravel), PHP
@@ -93,7 +97,7 @@ When you run `workos install` without credentials, the CLI automatically provisi
9397

9498
```bash
9599
# Install with zero setup — environment provisioned automatically
96-
workos install
100+
npx workos@latest install
97101

98102
# Check your environment
99103
workos env list
@@ -560,13 +564,13 @@ workos install [options]
560564
561565
```bash
562566
# Interactive (recommended)
563-
npx workos
567+
npx workos@latest install
564568

565569
# Specify framework
566-
npx workos install --integration react-router
570+
npx workos@latest install --integration react-router
567571

568572
# With visual dashboard (experimental)
569-
npx workos dashboard
573+
npx workos@latest dashboard
570574

571575
# JSON output (explicit)
572576
workos org list --json --api-key sk_test_xxx
@@ -648,13 +652,13 @@ The CLI uses WorkOS Connect OAuth device flow for authentication:
648652
649653
```bash
650654
# Login (opens browser for authentication)
651-
workos auth login
655+
npx workos@latest auth login
652656

653657
# Check current auth status
654-
workos auth status
658+
npx workos@latest auth status
655659

656660
# Logout (clears stored credentials)
657-
workos auth logout
661+
npx workos@latest auth logout
658662
```
659663
660664
OAuth credentials are stored in the system keychain (with `~/.workos/credentials.json` fallback). Access tokens are not persisted long-term for security - users re-authenticate when tokens expire.
@@ -682,7 +686,7 @@ The installer collects anonymous usage telemetry to help improve the product:
682686
No code, credentials, or personal data is collected. Disable with:
683687
684688
```bash
685-
WORKOS_TELEMETRY=false npx workos
689+
WORKOS_TELEMETRY=false npx workos@latest install
686690
```
687691
688692
## Logs

src/commands/auth-status.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import chalk from 'chalk';
22
import { getCredentials, isTokenExpired } from '../lib/credentials.js';
33
import { getActiveEnvironment } from '../lib/config-store.js';
44
import { isJsonMode, outputJson } from '../utils/output.js';
5+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
56

67
function formatTimeRemaining(ms: number): string {
78
if (ms <= 0) return 'expired';
@@ -23,7 +24,7 @@ export async function runAuthStatus(): Promise<void> {
2324
return;
2425
}
2526
console.log(chalk.yellow('Not logged in'));
26-
console.log(chalk.dim('Run `workos auth login` to authenticate'));
27+
console.log(chalk.dim(`Run \`${formatWorkOSCommand('auth login')}\` to authenticate`));
2728
return;
2829
}
2930

src/commands/claim.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { createClaimNonce, UnclaimedEnvApiError } from '../lib/unclaimed-env-api
1313
import { logInfo, logError } from '../utils/debug.js';
1414
import { isJsonMode, outputJson, exitWithError } from '../utils/output.js';
1515
import { sleep } from '../lib/helper-functions.js';
16+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
1617

1718
const POLL_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
1819
const POLL_INTERVAL_MS = 5_000; // 5 seconds
@@ -48,7 +49,7 @@ export async function runClaim(): Promise<void> {
4849
outputJson({ status: 'already_claimed', message: 'Environment already claimed!' });
4950
} else {
5051
clack.log.success('Environment already claimed!');
51-
clack.log.info('Run `workos auth login` to connect your account.');
52+
clack.log.info(`Run \`${formatWorkOSCommand('auth login')}\` to connect your account.`);
5253
}
5354
return;
5455
}
@@ -84,7 +85,7 @@ export async function runClaim(): Promise<void> {
8485
if (check.alreadyClaimed) {
8586
spinner.stop('Environment claimed!');
8687
markEnvironmentClaimed();
87-
clack.log.info('Run `workos auth login` to connect your account.');
88+
clack.log.info(`Run \`${formatWorkOSCommand('auth login')}\` to connect your account.`);
8889
return;
8990
}
9091
consecutiveFailures = 0;
@@ -95,7 +96,7 @@ export async function runClaim(): Promise<void> {
9596
// when the environment is claimed. Safe to promote to sandbox.
9697
spinner.stop('Claim token is invalid or expired.');
9798
markEnvironmentClaimed();
98-
clack.log.warn('Run `workos auth login` to set up your environment.');
99+
clack.log.warn(`Run \`${formatWorkOSCommand('auth login')}\` to set up your environment.`);
99100
return;
100101
}
101102
consecutiveFailures++;

src/commands/login.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { logInfo, logError } from '../utils/debug.js';
88
import { fetchStagingCredentials } from '../lib/staging-api.js';
99
import { getConfig, saveConfig } from '../lib/config-store.js';
1010
import type { CliConfig } from '../lib/config-store.js';
11+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
1112

1213
/**
1314
* Parse JWT payload
@@ -110,7 +111,7 @@ export async function runLogin(): Promise<void> {
110111
if (getAccessToken()) {
111112
const creds = getCredentials();
112113
console.log(chalk.green(`Already logged in as ${creds?.email ?? 'unknown'}`));
113-
console.log(chalk.dim('Run `workos auth logout` to log out'));
114+
console.log(chalk.dim(`Run \`${formatWorkOSCommand('auth logout')}\` to log out`));
114115
return;
115116
}
116117

@@ -124,7 +125,7 @@ export async function runLogin(): Promise<void> {
124125
updateTokens(result.accessToken, result.expiresAt, result.refreshToken);
125126
logInfo('[login] Session refreshed via refresh token');
126127
console.log(chalk.green(`Already logged in as ${existingCreds.email ?? 'unknown'}`));
127-
console.log(chalk.dim('Run `workos auth logout` to log out'));
128+
console.log(chalk.dim(`Run \`${formatWorkOSCommand('auth logout')}\` to log out`));
128129
return;
129130
}
130131
} catch {

src/doctor/checks/ai-analysis.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { getCredentials, isTokenExpired, updateTokens, diagnoseCredentials } fro
44
import { refreshAccessToken } from '../../lib/token-refresh-client.js';
55
import { buildDoctorPrompt, type AnalysisContext } from '../agent-prompt.js';
66
import type { AiAnalysis, AiFinding } from '../types.js';
7+
import { formatWorkOSCommand } from '../../utils/command-invocation.js';
78

89
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
910

@@ -61,10 +62,11 @@ async function callModel(prompt: string, model: string): Promise<string> {
6162
if (!creds) throw new Error('Not authenticated');
6263

6364
if (isTokenExpired(creds)) {
64-
if (!creds.refreshToken) throw new Error('Session expired — run `workos auth login` to re-authenticate');
65+
if (!creds.refreshToken)
66+
throw new Error(`Session expired — run \`${formatWorkOSCommand('auth login')}\` to re-authenticate`);
6567
const result = await refreshAccessToken(getAuthkitDomain(), getCliAuthClientId());
6668
if (!result.success || !result.accessToken || !result.expiresAt) {
67-
throw new Error('Session expired — run `workos auth login` to re-authenticate');
69+
throw new Error(`Session expired — run \`${formatWorkOSCommand('auth login')}\` to re-authenticate`);
6870
}
6971
updateTokens(result.accessToken, result.expiresAt, result.refreshToken);
7072
creds = getCredentials()!;
@@ -111,7 +113,7 @@ export async function checkAiAnalysis(context: AnalysisContext, options: { skipA
111113
process.stderr.write(` ${line}\n`);
112114
}
113115
process.stderr.write('\n');
114-
return skippedResult('Not authenticated — run `workos auth login` for AI-powered analysis');
116+
return skippedResult(`Not authenticated — run \`${formatWorkOSCommand('auth login')}\` for AI-powered analysis`);
115117
}
116118

117119
const startTime = Date.now();

src/lib/agent-interface.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { analytics } from '../utils/analytics.js';
1111
import { INSTALLER_INTERACTION_EVENT_NAME } from './constants.js';
1212
import { LINTING_TOOLS } from './safe-tools.js';
1313
import { getLlmGatewayUrlFromHost } from '../utils/urls.js';
14+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
1415
import { getConfig } from './settings.js';
1516
import { getCredentials, hasCredentials } from './credentials.js';
1617
import { ensureValidToken } from './token-refresh.js';
@@ -383,12 +384,12 @@ export async function initializeAgent(config: AgentConfig, options: InstallerOpt
383384
} else if (!options.skipAuth && !options.local) {
384385
// Check/refresh authentication for production (unless skipping auth)
385386
if (!hasCredentials()) {
386-
throw new Error('Not authenticated. Run `workos auth login` to authenticate.');
387+
throw new Error(`Not authenticated. Run \`${formatWorkOSCommand('auth login')}\` to authenticate.`);
387388
}
388389

389390
const creds = getCredentials();
390391
if (!creds) {
391-
throw new Error('Not authenticated. Run `workos auth login` to authenticate.');
392+
throw new Error(`Not authenticated. Run \`${formatWorkOSCommand('auth login')}\` to authenticate.`);
392393
}
393394

394395
// Check if we have refresh token capability and proxy is not disabled
@@ -409,7 +410,7 @@ export async function initializeAgent(config: AgentConfig, options: InstallerOpt
409410
onRefreshExpired: () => {
410411
logError('[agent-interface] Session expired, refresh token invalid');
411412
options.emitter?.emit('error', {
412-
message: 'Session expired. Run `workos auth login` to re-authenticate.',
413+
message: `Session expired. Run \`${formatWorkOSCommand('auth login')}\` to re-authenticate.`,
413414
});
414415
},
415416
},
@@ -426,9 +427,9 @@ export async function initializeAgent(config: AgentConfig, options: InstallerOpt
426427
// No refresh token OR proxy disabled - fall back to old behavior (5 min limit)
427428
if (!creds.refreshToken) {
428429
logWarn('[agent-interface] No refresh token available, session limited to 5 minutes');
429-
logWarn('[agent-interface] Run `workos auth login` to enable extended sessions');
430+
logWarn(`[agent-interface] Run \`${formatWorkOSCommand('auth login')}\` to enable extended sessions`);
430431
options.emitter?.emit('status', {
431-
message: 'Note: Run `workos auth login` to enable extended sessions',
432+
message: `Note: Run \`${formatWorkOSCommand('auth login')}\` to enable extended sessions`,
432433
});
433434
} else {
434435
logWarn('[agent-interface] Proxy disabled via INSTALLER_DISABLE_PROXY');

src/lib/credential-proxy.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { logInfo, logError, logWarn } from '../utils/debug.js';
1010
import { getCredentials, updateTokens, type Credentials } from './credentials.js';
1111
import { analytics } from '../utils/analytics.js';
1212
import { refreshAccessToken } from './token-refresh-client.js';
13+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
1314

1415
export interface RefreshConfig {
1516
/** AuthKit domain for refresh endpoint */
@@ -286,7 +287,7 @@ async function handleRequest(
286287
res.end(
287288
JSON.stringify({
288289
error: 'credentials_unavailable',
289-
message: 'Not authenticated. Run `workos auth login` first.',
290+
message: `Not authenticated. Run \`${formatWorkOSCommand('auth login')}\` first.`,
290291
}),
291292
);
292293
return;

src/lib/device-auth.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ interface TokenResponse {
4444

4545
interface AuthErrorResponse {
4646
error: string;
47+
error_description?: string;
4748
}
4849

4950
export class DeviceAuthError extends Error {
@@ -126,10 +127,13 @@ export async function pollForToken(
126127
const startTime = Date.now();
127128
let pollInterval = (options.interval || DEFAULT_POLL_INTERVAL_SECONDS) * 1000;
128129
const tokenUrl = `${options.authkitDomain}/oauth2/token`;
130+
let pollCount = 0;
131+
let lastPollSummary = 'no token response received';
129132

130133
logInfo('[device-auth] Starting token polling, timeout:', timeoutMs);
131134
while (Date.now() - startTime < timeoutMs) {
132135
await sleep(pollInterval);
136+
pollCount++;
133137
options.onPoll?.();
134138

135139
let res: Response;
@@ -159,19 +163,23 @@ export async function pollForToken(
159163
let data;
160164
try {
161165
data = await res.json();
162-
} catch {
163-
logError('[device-auth] Invalid JSON response from auth server');
164-
throw new DeviceAuthError('Invalid response from auth server');
166+
} catch (error) {
167+
const message = error instanceof Error ? error.message : String(error);
168+
logError('[device-auth] Invalid JSON response from auth server:', message);
169+
throw new DeviceAuthError(`Invalid response from auth server: ${message}`);
165170
}
166171

167-
logInfo('[device-auth] Token poll response:', res.status, (data as AuthErrorResponse)?.error ?? 'success');
172+
const errorData = data as AuthErrorResponse;
173+
const elapsedMs = Date.now() - startTime;
174+
lastPollSummary = res.ok
175+
? `${res.status} success`
176+
: `${res.status} ${errorData.error ?? 'unknown_error'}${errorData.error_description ? ` (${errorData.error_description})` : ''}`;
177+
logInfo('[device-auth] Token poll response:', `attempt=${pollCount}`, `elapsedMs=${elapsedMs}`, lastPollSummary);
168178
if (res.ok) {
169179
logInfo('[device-auth] Token received successfully');
170180
return parseTokenResponse(data as TokenResponse);
171181
}
172182

173-
const errorData = data as AuthErrorResponse;
174-
175183
if (errorData.error === 'authorization_pending') {
176184
continue;
177185
}
@@ -187,8 +195,10 @@ export async function pollForToken(
187195
throw new DeviceAuthError(`Token error: ${errorData.error}`);
188196
}
189197

190-
logError('[device-auth] Authentication timed out');
191-
throw new DeviceAuthError(`Authentication timed out after ${Math.round(timeoutMs / 1000)} seconds`);
198+
logError('[device-auth] Authentication timed out, last poll:', lastPollSummary);
199+
throw new DeviceAuthError(
200+
`Authentication timed out after ${Math.round(timeoutMs / 1000)} seconds (last token response: ${lastPollSummary})`,
201+
);
192202
}
193203

194204
function parseTokenResponse(data: TokenResponse): DeviceAuthResult {

src/lib/ensure-auth.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { runLogin } from '../commands/login.js';
99
import { logInfo } from '../utils/debug.js';
1010
import { isNonInteractiveEnvironment } from '../utils/environment.js';
1111
import { exitWithAuthRequired } from '../utils/exit-codes.js';
12+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
1213

1314
export interface EnsureAuthResult {
1415
/** Whether auth is now valid */
@@ -78,7 +79,7 @@ export async function ensureAuthenticated(): Promise<EnsureAuthResult> {
7879
clearCredentials();
7980
if (isNonInteractiveEnvironment()) {
8081
exitWithAuthRequired(
81-
'Session expired. Run `workos auth login` in an interactive terminal to re-authenticate.',
82+
`Session expired. Run \`${formatWorkOSCommand('auth login')}\` in an interactive terminal to re-authenticate.`,
8283
);
8384
}
8485
logInfo('[ensure-auth] Refresh token expired, triggering login');
@@ -91,7 +92,7 @@ export async function ensureAuthenticated(): Promise<EnsureAuthResult> {
9192
// Network or server error - keep credentials intact for retry
9293
if (isNonInteractiveEnvironment()) {
9394
exitWithAuthRequired(
94-
`Authentication refresh failed (${refreshResult.errorType}). Run \`workos auth login\` in an interactive terminal.`,
95+
`Authentication refresh failed (${refreshResult.errorType}). Run \`${formatWorkOSCommand('auth login')}\` in an interactive terminal.`,
9596
);
9697
}
9798
logInfo(`[ensure-auth] Refresh failed (${refreshResult.errorType}), triggering login`);
@@ -105,7 +106,9 @@ export async function ensureAuthenticated(): Promise<EnsureAuthResult> {
105106
// Case 4: No refresh token available — clear stale creds, must login
106107
clearCredentials();
107108
if (isNonInteractiveEnvironment()) {
108-
exitWithAuthRequired('Session expired. Run `workos auth login` in an interactive terminal to re-authenticate.');
109+
exitWithAuthRequired(
110+
`Session expired. Run \`${formatWorkOSCommand('auth login')}\` in an interactive terminal to re-authenticate.`,
111+
);
109112
}
110113
logInfo('[ensure-auth] No refresh token, triggering login');
111114
await runLogin();

src/lib/token-refresh-client.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import { logInfo, logError } from '../utils/debug.js';
66
import { getCredentials } from './credentials.js';
7+
import { formatWorkOSCommand } from '../utils/command-invocation.js';
78

89
export interface RefreshResult {
910
success: boolean;
@@ -73,7 +74,7 @@ export async function refreshAccessToken(authkitDomain: string, clientId: string
7374
if (errorData.error === 'invalid_grant') {
7475
return {
7576
success: false,
76-
error: 'Session expired. Run `workos auth login` to re-authenticate.',
77+
error: `Session expired. Run \`${formatWorkOSCommand('auth login')}\` to re-authenticate.`,
7778
errorType: 'invalid_grant',
7879
};
7980
}

0 commit comments

Comments
 (0)