From 5043773f9bde58d28caa22a69a35121cbd122ee0 Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 10 Apr 2026 13:05:49 +0200 Subject: [PATCH 1/3] feat: fix auth login validation and add auth whoami command - auth login now validates against /api/v1/auth/me (works with API keys) instead of /platform/me (JWT-only, always failed for CLI users) - Shows org name, key name, and plan tier on successful login - New auth whoami command displays full identity, plan, usage, and rate limits Made-with: Cursor --- src/commands/auth/login.ts | 16 ++++++---- src/commands/auth/whoami.ts | 60 +++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/commands/auth/whoami.ts diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index a047390..76ca678 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -6,7 +6,7 @@ import * as readline from 'node:readline' export default class AuthLogin extends Command { static description = 'Authenticate with the DevHelm API' - static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token sk_live_...'] + static examples = ['<%= config.bin %> auth login', '<%= config.bin %> auth login --token dh_live_...'] static flags = { ...globalFlags, token: Flags.string({description: 'API token (skips interactive prompt)'}), @@ -25,12 +25,18 @@ export default class AuthLogin extends Command { const client = createApiClient({baseUrl: apiUrl, token}) try { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const me = await checkedFetch(client.GET('/platform/me' as any, {} as any)) + const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any)) // eslint-disable-next-line @typescript-eslint/no-explicit-any - const email = (me as any)?.data?.email ?? (me as any)?.email + const me = (resp as any)?.data ?? resp + saveContext({name: flags.name, apiUrl, token}, true) - this.log(`\nAuthenticated as ${email}`) - this.log(`Context '${flags.name}' saved to ~/.devhelm/contexts.json`) + this.log('') + this.log(` Authenticated successfully.`) + this.log(` Organization: ${me.organization?.name ?? 'unknown'} (ID: ${me.organization?.id ?? '?'})`) + this.log(` Key: ${me.key?.name ?? 'unknown'}`) + this.log(` Plan: ${me.plan?.tier ?? 'unknown'}`) + this.log('') + this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`) } catch { this.error('Invalid token. Authentication failed.', {exit: 2}) } diff --git a/src/commands/auth/whoami.ts b/src/commands/auth/whoami.ts new file mode 100644 index 0000000..f85713a --- /dev/null +++ b/src/commands/auth/whoami.ts @@ -0,0 +1,60 @@ +import {Command} from '@oclif/core' +import {globalFlags, buildClient} from '../../lib/base-command.js' +import {checkedFetch} from '../../lib/api-client.js' +import {formatOutput, OutputFormat} from '../../lib/output.js' + +export default class AuthWhoami extends Command { + static description = 'Show current API key identity, organization, plan, and rate limits' + static examples = ['<%= config.bin %> auth whoami', '<%= config.bin %> auth whoami --output json'] + static flags = {...globalFlags} + + async run() { + const {flags} = await this.parse(AuthWhoami) + const client = buildClient(flags) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any)) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const me = (resp as any)?.data ?? resp + + const format = flags.output as OutputFormat + if (format === 'json' || format === 'yaml') { + this.log(formatOutput(me, format)) + return + } + + const k = me.key ?? {} + const o = me.organization ?? {} + const p = me.plan ?? {} + const r = me.rateLimits ?? {} + + this.log('') + this.log(' API Key') + this.log(` Name: ${k.name ?? '–'} ID: ${k.id ?? '–'}`) + this.log(` Created: ${k.createdAt ?? '–'} Expires: ${k.expiresAt ?? 'never'}`) + this.log(` Last used: ${k.lastUsedAt ?? 'never'}`) + this.log('') + this.log(' Organization') + this.log(` Name: ${o.name ?? '–'} ID: ${o.id ?? '–'}`) + this.log('') + this.log(' Plan') + this.log(` Tier: ${p.tier ?? '–'} Status: ${p.subscriptionStatus ?? '–'} Trial: ${p.trialActive ? `active (expires ${p.trialExpiresAt})` : 'no'}`) + this.log('') + this.log(' Rate Limits') + this.log(` Limit: ${r.requestsPerMinute ?? '–'} req/min Remaining: ${r.remaining ?? '–'} Window: ${r.windowMs ? `${r.windowMs / 1000}s` : '–'}`) + + const usage = p.usage as Record | undefined + const entitlements = p.entitlements as Record | undefined + if (usage && entitlements) { + this.log('') + this.log(' Usage') + for (const [key, used] of Object.entries(usage)) { + const limit = entitlements[key]?.value + const limitStr = limit != null && limit < Number.MAX_SAFE_INTEGER ? String(limit) : '∞' + const label = key.replace(/\./g, ' ').replace(/_/g, ' ') + this.log(` ${label}: ${used} / ${limitStr}`) + } + } + + this.log('') + } +} From afcd637ee4aa42383faae00daf11a825bd3a01d7 Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 10 Apr 2026 13:21:03 +0200 Subject: [PATCH 2/3] fix: graceful fallback in auth login for non-API-key tokens Tries /api/v1/auth/me first (rich identity for API keys), falls back to /api/v1/dashboard/overview for dev tokens and JWTs. Made-with: Cursor --- src/commands/auth/login.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/commands/auth/login.ts b/src/commands/auth/login.ts index 76ca678..b979b6f 100644 --- a/src/commands/auth/login.ts +++ b/src/commands/auth/login.ts @@ -23,6 +23,9 @@ export default class AuthLogin extends Command { const apiUrl = flags['api-url'] || resolveApiUrl() this.log('Validating token...') const client = createApiClient({baseUrl: apiUrl, token}) + + // Try /api/v1/auth/me first (API key — returns rich identity info). + // Falls back to /api/v1/dashboard/overview for non-API-key tokens (dev tokens, JWTs). try { // eslint-disable-next-line @typescript-eslint/no-explicit-any const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any)) @@ -37,6 +40,18 @@ export default class AuthLogin extends Command { this.log(` Plan: ${me.plan?.tier ?? 'unknown'}`) this.log('') this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`) + return + } catch { + // /auth/me failed — might be a non-API-key token; try basic validation + } + + try { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + await checkedFetch(client.GET('/api/v1/dashboard/overview' as any, {} as any)) + saveContext({name: flags.name, apiUrl, token}, true) + this.log('') + this.log(` Authenticated successfully.`) + this.log(` Context '${flags.name}' saved to ~/.devhelm/contexts.json`) } catch { this.error('Invalid token. Authentication failed.', {exit: 2}) } From 6806779c386c57a218cffde08c3b0049a05dec8b Mon Sep 17 00:00:00 2001 From: caballeto Date: Fri, 10 Apr 2026 13:28:56 +0200 Subject: [PATCH 3/3] refactor: rename auth whoami to auth me Made-with: Cursor --- src/commands/auth/{whoami.ts => me.ts} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename src/commands/auth/{whoami.ts => me.ts} (92%) diff --git a/src/commands/auth/whoami.ts b/src/commands/auth/me.ts similarity index 92% rename from src/commands/auth/whoami.ts rename to src/commands/auth/me.ts index f85713a..120e30d 100644 --- a/src/commands/auth/whoami.ts +++ b/src/commands/auth/me.ts @@ -3,13 +3,13 @@ import {globalFlags, buildClient} from '../../lib/base-command.js' import {checkedFetch} from '../../lib/api-client.js' import {formatOutput, OutputFormat} from '../../lib/output.js' -export default class AuthWhoami extends Command { +export default class AuthMe extends Command { static description = 'Show current API key identity, organization, plan, and rate limits' - static examples = ['<%= config.bin %> auth whoami', '<%= config.bin %> auth whoami --output json'] + static examples = ['<%= config.bin %> auth me', '<%= config.bin %> auth me --output json'] static flags = {...globalFlags} async run() { - const {flags} = await this.parse(AuthWhoami) + const {flags} = await this.parse(AuthMe) const client = buildClient(flags) // eslint-disable-next-line @typescript-eslint/no-explicit-any const resp = await checkedFetch(client.GET('/api/v1/auth/me' as any, {} as any))