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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions