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.

Metadata

Metadata

Assignees

Labels

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