Skip to content

Interactive login prompt doesn't strip quotes / TRACEROOT_API_KEY= prefix from the pasted API key #25

Description

@dark-sorceror

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 itTRACEROOT_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

  1. Run traceroot login in an interactive terminal (no --api-key, no env/config key set).
  2. 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").
  3. Login calls whoami with Authorization: Bearer TRACEROOT_API_KEY="tr_abc123" (quotes and prefix intact) → the request is rejected and no config is written.
  4. 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.

  1. 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;
    }
  2. 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,
    );
  3. The interactive prompt does not normalize. src/commands/login.ts:50
    apiKey = (await deps.promptHidden("API key: ")).trim();   // only trims — quotes/prefix survive
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions