Summary
The interactive login prompt is the only API-key input path that does not normalize what the user types. Every other source — the --api-key flag, --env-file, process env, the config file, and an auto-discovered .env — runs the value through normalizeApiKey(), which strips a pasted TRACEROOT_API_KEY= assignment prefix (optionally export -prefixed) and any surrounding quotes. The interactive prompt only calls .trim(), so a key pasted exactly as the dashboard presents it — TRACEROOT_API_KEY="tr_..." — or simply wrapped in quotes ("tr_...") is sent verbatim and authentication fails.
The result is a confusing inconsistency: traceroot login --api-key 'TRACEROOT_API_KEY="tr_abc123"' succeeds, but pasting that same string into the interactive API key: prompt fails.
How to replicate
- Run
traceroot login in an interactive terminal (no --api-key, no env/config key set).
- At the
API key: prompt, paste the value the dashboard hands you to copy: TRACEROOT_API_KEY="tr_abc123" (or just a quote-wrapped key, "tr_abc123").
- Login calls
whoami with Authorization: Bearer TRACEROOT_API_KEY="tr_abc123" (quotes and prefix intact) → the request is rejected and no config is written.
- For contrast, the flag form normalizes correctly:
traceroot login --api-key 'TRACEROOT_API_KEY="tr_abc123"' logs in fine.
Root cause (confirmed)
The normalizer exists and is wired into the resolution chain, but the interactive prompt bypasses it.
- The normalizer already handles every form.
src/config/resolve.ts:50
function normalizeApiKey(value: string): string {
let key = value.trim();
const assignment = key.match(/^(?:export\s+)?TRACEROOT_API_KEY\s*=\s*(.*)$/i);
if (assignment?.[1] !== undefined) {
key = assignment[1].trim();
}
if (key.length >= 2) {
const first = key[0];
const last = key[key.length - 1];
if (first === last && (first === '"' || first === "'")) {
key = key.slice(1, -1);
}
}
return key;
}
- It is applied to every resolved source.
src/config/resolve.ts:108
const apiKey = firstPresent(
[
{ value: flags.apiKey, source: "flag" },
{ value: fileMap.TRACEROOT_API_KEY, source: "env-file" },
{ value: env.TRACEROOT_API_KEY, source: "env" },
{ value: config.api_key, source: "config" },
{ value: autoEnv.TRACEROOT_API_KEY, source: "auto-env-file" },
],
normalizeApiKey,
);
- The interactive prompt does not normalize.
src/commands/login.ts:50
apiKey = (await deps.promptHidden("API key: ")).trim(); // only trims — quotes/prefix survive
- The unnormalized value flows straight into the auth header.
src/api/client.ts:56
authorization: `Bearer ${opts.apiKey}`, // used as-is
Impact
- Worst case is the most common case: the value pasted is the one the product literally tells the user to copy (
TRACEROOT_API_KEY="..."), and that is exactly the value that fails on the interactive path.
- Inconsistent contract: identical input succeeds via
--api-key but fails via the prompt, with no hint as to why.
- Silent-ish failure: the user sees an auth rejection, not "your key had quotes in it," so the cause is non-obvious.
Acceptance criteria
- The interactive
login prompt normalizes its input identically to the flag/env/config paths: xyz, "xyz", 'xyz', TRACEROOT_API_KEY="xyz", and export TRACEROOT_API_KEY="xyz" all resolve to the bare xyz.
- Normalization logic lives in one shared place reused by both the resolution chain and the prompt (no duplicated regex/quote-stripping).
- A test feeds the interactive prompt a
TRACEROOT_API_KEY="tr_..." value and a "tr_..." value and asserts the bare key is what gets validated and persisted.
- Existing flag/env/config normalization behavior and tests remain unchanged.
Summary
The interactive
loginprompt is the only API-key input path that does not normalize what the user types. Every other source — the--api-keyflag,--env-file, process env, the config file, and an auto-discovered.env— runs the value throughnormalizeApiKey(), which strips a pastedTRACEROOT_API_KEY=assignment prefix (optionallyexport-prefixed) and any surrounding quotes. The interactive prompt only calls.trim(), so a key pasted exactly as the dashboard presents it —TRACEROOT_API_KEY="tr_..."— or simply wrapped in quotes ("tr_...") is sent verbatim and authentication fails.The result is a confusing inconsistency:
traceroot login --api-key 'TRACEROOT_API_KEY="tr_abc123"'succeeds, but pasting that same string into the interactiveAPI key:prompt fails.How to replicate
traceroot loginin an interactive terminal (no--api-key, no env/config key set).API key:prompt, paste the value the dashboard hands you to copy:TRACEROOT_API_KEY="tr_abc123"(or just a quote-wrapped key,"tr_abc123").whoamiwithAuthorization: Bearer TRACEROOT_API_KEY="tr_abc123"(quotes and prefix intact) → the request is rejected and no config is written.traceroot login --api-key 'TRACEROOT_API_KEY="tr_abc123"'logs in fine.Root cause (confirmed)
The normalizer exists and is wired into the resolution chain, but the interactive prompt bypasses it.
src/config/resolve.ts:50src/config/resolve.ts:108src/commands/login.ts:50src/api/client.ts:56Impact
TRACEROOT_API_KEY="..."), and that is exactly the value that fails on the interactive path.--api-keybut fails via the prompt, with no hint as to why.Acceptance criteria
loginprompt normalizes its input identically to the flag/env/config paths:xyz,"xyz",'xyz',TRACEROOT_API_KEY="xyz", andexport TRACEROOT_API_KEY="xyz"all resolve to the barexyz.TRACEROOT_API_KEY="tr_..."value and a"tr_..."value and asserts the bare key is what gets validated and persisted.