Skip to content

fix(project): guard credential/auto-auth secret file reads (#282)#283

Open
OkeyAmy wants to merge 1 commit into
TestSprite:mainfrom
OkeyAmy:fix/project-secret-file-guard
Open

fix(project): guard credential/auto-auth secret file reads (#282)#283
OkeyAmy wants to merge 1 commit into
TestSprite:mainfrom
OkeyAmy:fix/project-secret-file-guard

Conversation

@OkeyAmy

@OkeyAmy OkeyAmy commented Jul 24, 2026

Copy link
Copy Markdown

What

project credential and project auto-auth (both v0.4.0) read their --*-file flags with a raw readFileSync(path, 'utf8').trim() and no guard. A missing file, a directory, or an oversized file escaped as an unwrapped Node error:

  • exit 1 (generic) instead of 5 (validation)
  • a malformed --output json envelopeerror was a bare string, not the { code, message, nextAction } object the rest of the CLI emits
  • leaked fs internals (absolute path + errno)

test.ts already guards all of its file flags (--code-file, --plan-from, --plans, --steps), and #248 adds readPasswordFileGuarded for --password-file — but only on create/update. The four credential/auto-auth flags were left unguarded because those commands didn't exist when that fix was written.

Change

Generalize the guard into readSecretFileGuarded(path, flagName) and apply it to all six project.ts file-read sites:

Command Flag
project create / project update --password-file
project credential --credential-file
project auto-auth --password-file, --client-secret-file, --refresh-token-file

Missing / non-regular files now return VALIDATION_ERROR (exit 5); oversized files (> 64 KiB) return PAYLOAD_TOO_LARGE (exit 5). Each error carries the flag name, matching the existing --code-file contract.

Before / after

# before
$ testsprite project credential <id> --type "API key" --credential-file /nope --output json
{ "error": "ENOENT: no such file or directory, open '/nope'" }   # exit 1

# after
$ testsprite project credential <id> --type "API key" --credential-file /nope --output json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Invalid request.",
    "nextAction": "Flag `--credential-file` is invalid: must point to a readable file.",
    "requestId": "local",
    "details": { "field": "credential-file", "reason": "must point to a readable file", ... }
  }
}   # exit 5

Relationship to #248

This absorbs #248's --password-file guard into one shared helper covering all six sites, so the two don't need separate implementations. Happy to rebase around whichever lands first — if #248 merges, I'll drop the two overlapping password-file sites; if this merges first, #248 becomes redundant.

Notes / scope

  • Kept the existing dry-run behavior: create/update still skip the file read under --dry-run; credential/auto-auth validate the path under --dry-run (now a clean exit-5 error instead of a raw crash). Unifying the dry-run read ordering across all four commands is a reasonable follow-up but is out of scope here to keep the change focused.
  • No changes to the wire request shape or to any success path.

Testing

  • npm run typecheck, npm run lint, npm run format:check — clean
  • Full suite: 2037 passed, 2 skipped
  • Adds 11 tests: missing / directory / oversized inputs across all four new flags plus the two password-file sites, and the trimmed happy path
  • Verified live against the production API on main (v0.4.0)

Closes #282

Summary by CodeRabbit

  • Bug Fixes
    • Improved validation for secret file options used with project commands.
    • Missing paths, directories, and unreadable files now return clear validation errors instead of raw filesystem errors.
    • Oversized secret files are rejected with an appropriate payload-size error.
    • Prevented network requests when secret file validation fails.
    • Valid credential files are read, trimmed, and submitted correctly.

…e#282)

The v0.4.0 `project credential` and `project auto-auth` commands read
their `--*-file` flags with a raw `readFileSync(path).trim()`. A missing
file, a directory, or an oversized file escaped as an unwrapped Node error
(exit 1) that also broke the `--output json` envelope (bare `{"error":"ENOENT…"}`
instead of the structured `{code,message,nextAction}`) and leaked fs internals.

Generalize the guard into `readSecretFileGuarded(path, flagName)` — mirroring
the `--code-file` guard in test.ts and PR TestSprite#248's `readPasswordFileGuarded` —
and apply it to all six project.ts file-read sites:
  - project create/update  --password-file
  - project credential      --credential-file
  - project auto-auth       --password-file / --client-secret-file / --refresh-token-file

Missing/non-regular files now return VALIDATION_ERROR (exit 5) and oversized
files return PAYLOAD_TOO_LARGE (exit 5), each carrying the flag name — the
same contract every other file flag already honors. Adds 11 tests covering
missing/directory/oversized inputs across all four new flags plus the two
password-file sites, and the trimmed happy path.

Closes TestSprite#282
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Project secret-file flags now use a shared guarded reader that validates paths, rejects oversized files, and returns structured errors. The guard is wired into create, update, credential, and auto-auth commands, with tests covering failures, network suppression, size limits, and successful trimmed reads.

Changes

Project secret-file validation

Layer / File(s) Summary
Secret-file guard implementation
src/commands/project.ts
Adds MAX_SECRET_FILE_BYTES, path resolution, regular-file checks, size enforcement, and typed validation errors for secret-file reads.
Command-path integration
src/commands/project.ts
Routes password, credential, client-secret, and refresh-token file options through the guarded reader in create, update, credential, and auto-auth commands.
Guard behavior tests
src/commands/project.test.ts
Tests missing and directory files, oversized credentials, no-network failures, and successful trimmed credential submission.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: ruili-testsprite, zeshi-du

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: guarding secret file reads for project credential/auto-auth.
Linked Issues check ✅ Passed The implementation matches #282 by guarding the affected file flags, returning structured validation errors, and covering the size limit.
Out of Scope Changes check ✅ Passed The changes stay focused on secret file-read guarding and related tests, with no clear unrelated additions.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/commands/project.ts (1)

467-472: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Guarded credential-file read runs before the --dry-run check — violates the dry-run filesystem contract.

readSecretFileGuarded is invoked at Line 471, but opts.dryRun isn't checked until Line 487. Unlike runCreate/runUpdate (which explicitly defer file reads until after the dry-run early return), runCredential --dry-run --credential-file <path> will statSync/readFileSync the file for real, and will throw a VALIDATION_ERROR/PAYLOAD_TOO_LARGE if the file is missing/oversized — even though dry-run is documented to be a pure offline preview.

🔧 Proposed fix — presence-check only, defer the real read past dry-run
-  // Resolve the credential value (flag or file). Required for every type
-  // except `public` (which clears it).
-  let credential = opts.credential;
-  if (credential === undefined && opts.credentialFile !== undefined) {
-    credential = readSecretFileGuarded(opts.credentialFile, 'credential-file');
-  }
-  if (opts.authType !== 'public' && (credential === undefined || credential === '')) {
+  // Presence-only check here — dry-run must not touch the filesystem.
+  // The actual file read happens below, after the dry-run early return.
+  const credentialSupplied =
+    (opts.credential !== undefined && opts.credential !== '') ||
+    opts.credentialFile !== undefined;
+  if (opts.authType !== 'public' && !credentialSupplied) {
     throw localValidationError(
       '--credential (or --credential-file) is required unless --type is "public"',
     );
   }

Then, after the if (opts.dryRun) { ... return sample; } block:

+  let credential = opts.credential;
+  if (credential === undefined && opts.credentialFile !== undefined) {
+    credential = readSecretFileGuarded(opts.credentialFile, 'credential-file');
+  }
+
   const body: Record<string, string> = { authType: opts.authType };
   if (opts.authType !== 'public' && credential !== undefined) body.credential = credential;

As per path instructions, "the CLI must skip network, credentials, and local filesystem work" during --dry-run, so this ordering needs to change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.ts` around lines 467 - 472, Move the guarded
credential-file read in runCredential so it occurs only after the opts.dryRun
early-return block. During dry-run, retain only the credential-file presence
information needed to build the preview and avoid invoking readSecretFileGuarded
or any filesystem access; preserve the existing real credential resolution for
non-dry-run execution.

Source: Path instructions

🧹 Nitpick comments (4)
src/commands/project.test.ts (2)

1516-1516: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stray review-tooling comment left in the test body.

// blindfold: manual — the on-disk fixture written above is " tok-from-file\n"; the guard trims it reads like a leftover annotation rather than an intentional code comment. Consider removing it or rewording as a normal explanatory comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.test.ts` at line 1516, Remove the stray “blindfold:
manual” annotation from the test body, or replace it with a concise normal
comment explaining the fixture’s whitespace and trimming behavior without
review-tooling terminology.

1428-1608: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test exercises --dry-run combined with a guarded --*-file flag for runCredential/runAutoAuth.

All new cases here call runCredential/runAutoAuth/runCreate/runUpdate without dryRun: true. Given the ordering bug flagged in src/commands/project.ts (credential/secret-file resolution currently runs before the dry-run check in runCredential and runAutoAuth), a test asserting that --dry-run --credential-file <missing-path> returns the canned sample without touching the filesystem would have caught this regression class.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.test.ts` around lines 1428 - 1608, Add coverage in the
“#282 — secret --*-file flags are guarded” suite for runCredential and
runAutoAuth with dryRun: true and missing credential/secret-file paths. Assert
each returns its canned dry-run sample successfully without reading the
filesystem or invoking the network, preserving the existing guarded-file
validation cases for non-dry-run execution.

Source: Path instructions

src/commands/project.ts (2)

1101-1147: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Raw filesystem error text is embedded in the structured envelope's details.error.

secretFileError(flagName, 'must point to a readable file', { path: absolute, error: message }) forwards the raw err.message from statSync (e.g. an ENOENT/OS-formatted string) straight into the response details. The top-level message/nextAction are clean, but automation or UI code that surfaces details.error verbatim would still expose the underlying OS error text and absolute path, which is what the doc's "rather than leaking raw filesystem errors" guidance is trying to avoid.

♻️ Suggested tweak — drop the raw OS message, keep a generic reason
   } catch (err) {
-    const message = err instanceof Error ? err.message : String(err);
     throw secretFileError(flagName, 'must point to a readable file', {
       path: absolute,
-      error: message,
     });
   }

As per path instructions, "the CLI should validate these flags locally and return structured validation errors rather than leaking raw filesystem errors."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.ts` around lines 1101 - 1147, Update
readSecretFileGuarded and secretFileError usage so statSync failures do not
include the raw filesystem error message or absolute path in the structured
details. Keep the structured validation error and generic “must point to a
readable file” reason, but remove the error text and path fields from this
failure response.

Source: Path instructions


1088-1147: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the guarded file-read helpers into a shared module.

test.ts has multiple stat/read guards for --code-file, steps, plan files, and output paths, while project.ts now adds a guarded secret-file reader. Lifting these into src/lib will keep file validation consistent and make future --*-file guards easier to add and test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.ts` around lines 1088 - 1147, Extract
readSecretFileGuarded, secretFileError, and MAX_SECRET_FILE_BYTES from
project.ts into a shared src/lib module, then update project.ts to import and
use them. Consolidate the existing guarded file stat/read helpers from test.ts
into the same module where applicable, preserving their validation behavior,
ApiError envelopes, size limits, and flag-specific details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/commands/project.ts`:
- Around line 1101-1131: Update readSecretFileGuarded so the final readFileSync
operation is wrapped in the same structured error handling as statSync. Convert
read failures into secretFileError(flagName, 'must point to a readable file',
including the absolute path and underlying error message), while preserving the
existing validation and trimmed successful-read behavior.
- Around line 577-592: Move the password, clientSecret, and refreshToken
resolution using readSecretFileGuarded out of the initial setup in runAutoAuth
and place it after the opts.dryRun early return, ensuring dry-run performs no
filesystem access. Restructure the subsequent request-body construction to use
the resolved values without duplicate maybe calls.

---

Outside diff comments:
In `@src/commands/project.ts`:
- Around line 467-472: Move the guarded credential-file read in runCredential so
it occurs only after the opts.dryRun early-return block. During dry-run, retain
only the credential-file presence information needed to build the preview and
avoid invoking readSecretFileGuarded or any filesystem access; preserve the
existing real credential resolution for non-dry-run execution.

---

Nitpick comments:
In `@src/commands/project.test.ts`:
- Line 1516: Remove the stray “blindfold: manual” annotation from the test body,
or replace it with a concise normal comment explaining the fixture’s whitespace
and trimming behavior without review-tooling terminology.
- Around line 1428-1608: Add coverage in the “#282 — secret --*-file flags are
guarded” suite for runCredential and runAutoAuth with dryRun: true and missing
credential/secret-file paths. Assert each returns its canned dry-run sample
successfully without reading the filesystem or invoking the network, preserving
the existing guarded-file validation cases for non-dry-run execution.

In `@src/commands/project.ts`:
- Around line 1101-1147: Update readSecretFileGuarded and secretFileError usage
so statSync failures do not include the raw filesystem error message or absolute
path in the structured details. Keep the structured validation error and generic
“must point to a readable file” reason, but remove the error text and path
fields from this failure response.
- Around line 1088-1147: Extract readSecretFileGuarded, secretFileError, and
MAX_SECRET_FILE_BYTES from project.ts into a shared src/lib module, then update
project.ts to import and use them. Consolidate the existing guarded file
stat/read helpers from test.ts into the same module where applicable, preserving
their validation behavior, ApiError envelopes, size limits, and flag-specific
details.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d200ba6-1cf0-4890-a8ec-64fd0e287a0f

📥 Commits

Reviewing files that changed from the base of the PR and between fe07bc9 and 080a929.

📒 Files selected for processing (2)
  • src/commands/project.test.ts
  • src/commands/project.ts

Comment thread src/commands/project.ts
Comment on lines 577 to 592
// Resolve secrets from --*-file variants so they stay out of shell history.
const password =
opts.password ??
(opts.passwordFile !== undefined ? readFileSync(opts.passwordFile, 'utf8').trim() : undefined);
(opts.passwordFile !== undefined
? readSecretFileGuarded(opts.passwordFile, 'password-file')
: undefined);
const clientSecret =
opts.clientSecret ??
(opts.clientSecretFile !== undefined
? readFileSync(opts.clientSecretFile, 'utf8').trim()
? readSecretFileGuarded(opts.clientSecretFile, 'client-secret-file')
: undefined);
const refreshToken =
opts.refreshToken ??
(opts.refreshTokenFile !== undefined
? readFileSync(opts.refreshTokenFile, 'utf8').trim()
? readSecretFileGuarded(opts.refreshTokenFile, 'refresh-token-file')
: undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Same dry-run bypass: secret-file resolution runs before the --dry-run check in runAutoAuth.

password/clientSecret/refreshToken are resolved (potentially reading files via readSecretFileGuarded) at Lines 578-592, but opts.dryRun isn't checked until Line 619. Any of --password-file, --client-secret-file, or --refresh-token-file will hit the real filesystem during --dry-run, contradicting the documented offline guarantee.

🔧 Proposed fix — move secret-file resolution after the dry-run early return
-  // Resolve secrets from --*-file variants so they stay out of shell history.
-  const password =
-    opts.password ??
-    (opts.passwordFile !== undefined
-      ? readSecretFileGuarded(opts.passwordFile, 'password-file')
-      : undefined);
-  const clientSecret =
-    opts.clientSecret ??
-    (opts.clientSecretFile !== undefined
-      ? readSecretFileGuarded(opts.clientSecretFile, 'client-secret-file')
-      : undefined);
-  const refreshToken =
-    opts.refreshToken ??
-    (opts.refreshTokenFile !== undefined
-      ? readSecretFileGuarded(opts.refreshTokenFile, 'refresh-token-file')
-      : undefined);
-
   const enabled = opts.disable !== true;
   const body: Record<string, unknown> = { enabled, method: opts.method, inject: opts.inject };

Then, immediately after if (opts.dryRun) { ... return sample; }:

+  // Resolve secrets from --*-file variants so they stay out of shell history.
+  // Deferred until after the dry-run return so preview mode never touches disk.
+  const password =
+    opts.password ??
+    (opts.passwordFile !== undefined
+      ? readSecretFileGuarded(opts.passwordFile, 'password-file')
+      : undefined);
+  const clientSecret =
+    opts.clientSecret ??
+    (opts.clientSecretFile !== undefined
+      ? readSecretFileGuarded(opts.clientSecretFile, 'client-secret-file')
+      : undefined);
+  const refreshToken =
+    opts.refreshToken ??
+    (opts.refreshTokenFile !== undefined
+      ? readSecretFileGuarded(opts.refreshTokenFile, 'refresh-token-file')
+      : undefined);
+  maybe('password', password);
+  maybe('clientSecret', clientSecret);
+  maybe('refreshToken', refreshToken);

(then remove the now-duplicate maybe('password', password) etc. calls further down, or restructure to build body in one place.)

As per path instructions, dry-run must skip local filesystem work.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.ts` around lines 577 - 592, Move the password,
clientSecret, and refreshToken resolution using readSecretFileGuarded out of the
initial setup in runAutoAuth and place it after the opts.dryRun early return,
ensuring dry-run performs no filesystem access. Restructure the subsequent
request-body construction to use the resolved values without duplicate maybe
calls.

Source: Path instructions

Comment thread src/commands/project.ts
Comment on lines +1101 to +1131
function readSecretFileGuarded(path: string, flagName: string): string {
const absolute = resolve(path);
let stat;
try {
stat = statSync(absolute);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw secretFileError(flagName, 'must point to a readable file', {
path: absolute,
error: message,
});
}

if (!stat.isFile()) {
throw secretFileError(flagName, 'must point to a regular file', { path: absolute });
}

if (stat.size > MAX_SECRET_FILE_BYTES) {
throw ApiError.fromEnvelope({
error: {
code: 'PAYLOAD_TOO_LARGE',
message: 'Secret file is too large.',
nextAction: `Flag \`--${flagName}\` is invalid: file must be at most ${MAX_SECRET_FILE_BYTES} bytes.`,
requestId: 'local',
details: { field: flagName, sizeBytes: stat.size, maxBytes: MAX_SECRET_FILE_BYTES },
},
});
}

return readFileSync(absolute, 'utf8').trim();
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Node.js fs.statSync succeed on a file the process lacks read permission for, while fs.readFileSync fails with EACCES?

💡 Result:

No, fs.statSync does not necessarily succeed if the process lacks read permission for the file. The success of both fs.statSync and fs.readFileSync depends on the operating system's permission model, and both may fail with EACCES if the process does not have the required access rights [1][2]. In POSIX-compliant systems, fs.statSync requires the process to have permission to traverse (execute/search) all directories in the path leading to the file [3]. If the process lacks the necessary permissions to traverse the path or access the file's metadata, fs.statSync will throw an EACCES error [1][3]. Similarly, fs.readFileSync requires read permission for the file itself, and will fail with EACCES if that permission is missing [1][2]. While some users might observe scenarios where metadata is accessible while file content is not, this is determined by the underlying file system permissions and the kernel's enforcement of them, not by a difference in how Node.js handles these specific operations [1]. If an EACCES error occurs, it indicates that the operating system has explicitly denied the request based on the current process's user/group identity and the file's security settings [4][1]. Node.js merely surfaces the error provided by the operating system [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate project.ts =="
fd -a 'project\.ts$' . | sed 's#^\./##'

echo "== inspect relevant helper and usages =="
file="$(fd 'project\.ts$' . | head -n 1)"
wc -l "$file"
sed -n '1060,1145p' "$file"

echo "== find readSecretFileGuarded usages =="
rg -n "readSecretFileGuarded|secretFileError|MAX_SECRET_FILE_BYTES|--dry-run|readFileSync|statSync" "$file" DOCUMENTATION.md 2>/dev/null || true

echo "== behavioral Node.fs stat/read permission probe =="
node - <<'JS'
const fs = require('fs');
const path = require('path');
const os = require('os');
const tmp = require('os').tmpdir();
let fn;
try {
  fn = path.join(tmp, `secret-mode-probe-${process.pid}-${process.hrtime.bigint().toString()}`);
  fs.writeFileSync(fn, 'secret');
  const before = fs.statSync(fn);
  fs.chmodSync(fn, 0);
  console.log('node', process.version);
  console.log('platform', process.platform);
  console.log('effectiveUid', process.geteuid());
  console.log('initial stat ok, isFile=' + before.isFile());
  try {
    const data = fs.readFileSync(fn, 'utf8').trim();
    console.log('readFileSync result:', data);
  } catch (err) {
    console.log('readFileSync error code=' + err.code + ' msg=' + err.message);
  }
  console.log('final stat after chmod 0');
  try {
    const post = fs.statSync(fn);
    console.log('stat ok, isFile=' + post.isFile());
  } catch (err) {
    console.log('stat error code=' + err.code + ' msg=' + err.message);
  }
} finally {
  try { fs.chmodSync(fn, 0o600); fs.unlinkSync(fn); } catch {}
}
JS

Repository: TestSprite/testsprite-cli

Length of output: 9913


Wrap the final secret-file read in this helper

statSync can succeed while readFileSync fails with EACCES when the file becomes unreadable after stat (for example, root running against a mode-0 file). Those failures currently surface as a raw filesystem error instead of the documented structured validation envelope.

🔧 Proposed fix — guard the final read too
-  return readFileSync(absolute, 'utf8').trim();
+  try {
+    return readFileSync(absolute, 'utf8').trim();
+  } catch (err) {
+    const message = err instanceof Error ? err.message : String(err);
+    throw secretFileError(flagName, 'must point to a readable file', {
+      path: absolute,
+      error: message,
+    });
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function readSecretFileGuarded(path: string, flagName: string): string {
const absolute = resolve(path);
let stat;
try {
stat = statSync(absolute);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw secretFileError(flagName, 'must point to a readable file', {
path: absolute,
error: message,
});
}
if (!stat.isFile()) {
throw secretFileError(flagName, 'must point to a regular file', { path: absolute });
}
if (stat.size > MAX_SECRET_FILE_BYTES) {
throw ApiError.fromEnvelope({
error: {
code: 'PAYLOAD_TOO_LARGE',
message: 'Secret file is too large.',
nextAction: `Flag \`--${flagName}\` is invalid: file must be at most ${MAX_SECRET_FILE_BYTES} bytes.`,
requestId: 'local',
details: { field: flagName, sizeBytes: stat.size, maxBytes: MAX_SECRET_FILE_BYTES },
},
});
}
return readFileSync(absolute, 'utf8').trim();
}
function readSecretFileGuarded(path: string, flagName: string): string {
const absolute = resolve(path);
let stat;
try {
stat = statSync(absolute);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw secretFileError(flagName, 'must point to a readable file', {
path: absolute,
error: message,
});
}
if (!stat.isFile()) {
throw secretFileError(flagName, 'must point to a regular file', { path: absolute });
}
if (stat.size > MAX_SECRET_FILE_BYTES) {
throw ApiError.fromEnvelope({
error: {
code: 'PAYLOAD_TOO_LARGE',
message: 'Secret file is too large.',
nextAction: `Flag \`--${flagName}\` is invalid: file must be at most ${MAX_SECRET_FILE_BYTES} bytes.`,
requestId: 'local',
details: { field: flagName, sizeBytes: stat.size, maxBytes: MAX_SECRET_FILE_BYTES },
},
});
}
try {
return readFileSync(absolute, 'utf8').trim();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
throw secretFileError(flagName, 'must point to a readable file', {
path: absolute,
error: message,
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/commands/project.ts` around lines 1101 - 1131, Update
readSecretFileGuarded so the final readFileSync operation is wrapped in the same
structured error handling as statSync. Convert read failures into
secretFileError(flagName, 'must point to a readable file', including the
absolute path and underlying error message), while preserving the existing
validation and trimmed successful-read behavior.

Source: Path instructions

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

1 participant