Skip to content

fix(project): credential/auto-auth file flags bypass the file-read guard (raw ENOENT, exit 1, malformed --output json) #282

Description

@OkeyAmy

Summary

Four file-reading flags added in v0.4.0 on project credential and project auto-auth read their file with a raw readFileSync(path, 'utf8').trim() and no guard. When the file is missing (or is a directory), the CLI throws an unwrapped Node error instead of the structured validation error every other file flag produces:

  • exit 1 (generic) instead of 5 (validation)
  • a malformed --output json envelopeerror is a bare string, not the { code, message, nextAction } object the rest of the CLI emits, so anything parsing --output json breaks
  • it leaks raw fs internals (absolute path + errno)
  • it also crashes under --dry-run, which is documented not to touch the filesystem

Affected flags

Command Flag Site
project credential --credential-file src/commands/project.ts:470
project auto-auth --password-file src/commands/project.ts:579
project auto-auth --client-secret-file src/commands/project.ts:583
project auto-auth --refresh-token-file src/commands/project.ts:588

All six use the same unguarded pattern:

credential = readFileSync(opts.credentialFile, 'utf8').trim();

Reproduction

The contrast is the bug — the same class of user error (bad input to a file flag) gets two different contracts:

# Guarded flag (correct): structured envelope, exit 5
$ testsprite test create --project x --type backend --name n --code-file /nope.py --output json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request.",
    "nextAction": "Flag `--code-file` is invalid: file does not exist: /nope.py.",
    "requestId": "local",
    "details": { "field": "code-file", ... }
  }
}
# exit 5

# Unguarded flag (bug): bare-string envelope, exit 1, leaks fs internals
$ testsprite project credential <project-id> --type "API key" --credential-file /nope --output json
{
  "error": "ENOENT: no such file or directory, open '/nope'"
}
# exit 1

--dry-run does not help — it reads the file (and crashes) before the dry-run banner:

$ testsprite project credential <project-id> --type "API key" --credential-file /nope --dry-run
Error: ENOENT: no such file or directory, open '/nope'
# exit 1

A directory argument surfaces a raw EISDIR the same way.

Why this is a gap and not already covered

  • src/commands/test.ts already guards all of its file flags (--code-file, --plan-from, --plans, --steps) — each returns a clean VALIDATION_ERROR / exit 5 with a field detail. That is the reference behavior.
  • PR Recovered: fix: bound pagination and guard password file reads (#61 by @merlinsantiago982-cmd) #248 adds readPasswordFileGuarded(), but it is hardcoded to --password-file and only wired into project create / project update (project.ts:213, :329). The four flags above are not covered — credential / auto-auth did not exist when that fix was written, so they survive its merge.
  • A repo-wide readFileSync sweep confirms these four are the only remaining unguarded user-flag file reads.

Suggested fix

Generalize the guard into a small helper and apply it to all six project.ts file-read sites (the two --password-file sites plus the four new ones):

function readSecretFileGuarded(path: string, flagName: string): string {
  // statSync -> not-found / not-a-file / too-large => VALIDATION_ERROR (exit 5),
  // field: flagName, requestId: 'local' — mirrors readPasswordFileGuarded (#248)
  // and the --code-file guard in test.ts.
}

Happy to sequence this however you prefer — fold it into #248, or land it right after so the helper covers --password-file too.

Environment: reproduced on current main (v0.4.0) against the production API, Node 20.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

acceptedTriaged and accepted — a PR for this will be reviewedin-progressAssigned and actively being worked on

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions