Skip to content

feat: add jsonfile credential provider for JSON file credential injection#366

Open
ychost wants to merge 1 commit intolarksuite:mainfrom
ychost:feat/jsonfile-credential-provider
Open

feat: add jsonfile credential provider for JSON file credential injection#366
ychost wants to merge 1 commit intolarksuite:mainfrom
ychost:feat/jsonfile-credential-provider

Conversation

@ychost
Copy link
Copy Markdown

@ychost ychost commented Apr 9, 2026

Summary

Add a new credential extension provider (jsonfile) that enables non-interactive credential injection via a JSON file, specified through the LARKSUITE_CLI_CREDENTIAL_FILE environment variable.

This is useful for CI/CD pipelines, Docker containers, and scenarios where interactive config init is not feasible.

Changes

  • New: extension/credential/jsonfile/jsonfile.go — JSON file credential provider implementing the credential.Provider interface
  • New: extension/credential/jsonfile/jsonfile_test.go — 27 comprehensive test cases with race detection
  • Modified: internal/envvars/envvars.go — Added LARKSUITE_CLI_CREDENTIAL_FILE constant
  • Modified: main.go — Blank import to activate the jsonfile provider
  • Modified: README.md, README.zh.md — Documentation for credential file usage

Key Features

  • Reads app_id, app_secret, brand, default_as, user_access_token, tenant_access_token, and refresh_token from a JSON file
  • Automatic UAT refresh via OAuth token endpoint when refresh_token is provided (with in-process caching)
  • Path validation: requires absolute path, rejects control characters
  • Proper BlockError handling for structured AI-agent-friendly error messages
  • Provider priority: env vars → jsonfile → default config (config init)

Test Plan

  • Unit tests pass (27 tests, all PASS with -race)
  • go mod tidy — no changes to go.mod/go.sum
  • golangci-lint run --new-from-rev=origin/main — 0 issues
  • go build ./... — compiles successfully
  • Manual local verification: LARKSUITE_CLI_CREDENTIAL_FILE=/path/to/cred.json lark-cli --version works

Related Issues

  • None

Summary by CodeRabbit

  • New Features

    • Added support for external JSON credential files via the LARKSUITE_CLI_CREDENTIAL_FILE environment variable, allowing users to bypass interactive credential setup.
  • Documentation

    • Updated README and Chinese README with new Credential File section documenting the JSON credential schema, required fields, and environment variable priority.

…tion

Add a new credential extension provider that reads app credentials from a
JSON file specified via LARKSUITE_CLI_CREDENTIAL_FILE environment variable.

This enables non-interactive credential injection by pointing to a JSON file
containing app_id, app_secret, brand, access tokens, and/or refresh_token.

Key features:
- Reads credentials from a JSON file (absolute path required)
- Supports app_id, app_secret, brand, default_as fields
- Supports user_access_token and tenant_access_token for direct token injection
- Supports refresh_token for automatic UAT refresh via OAuth endpoint
- In-process token caching with 30s pre-expiry refresh
- Path validation: rejects relative paths and control characters
- Comprehensive error handling with BlockError for structured feedback

Provider priority: env vars > jsonfile > default config (config init)

Files changed:
- extension/credential/jsonfile/jsonfile.go (new provider)
- extension/credential/jsonfile/jsonfile_test.go (27 tests)
- internal/envvars/envvars.go (new LARKSUITE_CLI_CREDENTIAL_FILE const)
- main.go (blank import to activate provider)
- README.md, README.zh.md (credential file documentation)
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.


ychost.yc seems not to be a GitHub user. You need a GitHub account to be able to sign the CLA. If you have already a GitHub account, please add the email address used for this commit to your account.
You have signed the CLA already but the status is still pending? Let us recheck it.

@github-actions github-actions bot added the size/M Single-domain feat or fix with limited business impact label Apr 9, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

A new JSON credential file provider is introduced, enabling credentials to be loaded from an external file via the LARKSUITE_CLI_CREDENTIAL_FILE environment variable. The implementation includes token refresh caching, field validation, account resolution, and comprehensive test coverage, alongside updated documentation.

Changes

Cohort / File(s) Summary
Documentation Updates
README.md, README.zh.md
Added "Credential File" section documenting the new LARKSUITE_CLI_CREDENTIAL_FILE environment variable, JSON credential schema, field requirements table, and precedence rules. Minor formatting adjustments to existing tables and URL escaping.
Credential Provider Implementation
extension/credential/jsonfile/jsonfile.go
New provider reads credentials from JSON file, parses app credentials and tokens, validates field combinations with credential.BlockError, supports user and tenant access tokens, implements mutex-protected token refresh caching with 30s refresh-ahead window, and handles OAuth token endpoint resolution.
Provider Tests
extension/credential/jsonfile/jsonfile_test.go
Comprehensive test suite covering account resolution (success, missing fields, defaults), identity inference, file validation, token resolution (direct and refreshed), caching behavior, and error handling for malformed JSON and invalid paths.
Core Module Integration
internal/envvars/envvars.go, main.go
Added CliCredentialFile environment variable constant and registered the new credential provider via side-effect import.

Sequence Diagram

sequenceDiagram
    actor CLI as CLI Request
    participant JF as JSON File<br/>Provider
    participant Cache as Refresh<br/>Cache
    participant OAuth as OAuth Token<br/>Endpoint
    
    CLI->>JF: ResolveToken(UAT or TAT)
    alt Direct Token Available
        JF->>JF: Return token from file
        JF-->>CLI: Token
    else Refresh Token Available
        JF->>Cache: Check cached token
        alt Cached & Valid
            Cache-->>JF: Return cached token
            JF-->>CLI: Token
        else Cache Miss or Expired
            JF->>OAuth: POST refresh_token grant
            OAuth-->>JF: {access_token, expires_in}
            JF->>Cache: Update cache + timestamp
            JF-->>CLI: Token
        end
    else Token Missing
        JF-->>CLI: nil
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Suggested Labels

size/L

Suggested Reviewers

  • liangshuo-1

Poem

🐰 A file of secrets, JSON-wrapped so tight,
With tokens cached and OAuth in sight!
The rabbit hops through credentials with care,
Refresh windows and validations everywhere! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a new JSON file credential provider, which is the primary objective of this PR.
Description check ✅ Passed The PR description follows the template with complete Summary, Changes, and Test Plan sections; all required information is present and well-structured.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 9, 2026

Greptile Summary

This PR adds a jsonfile credential provider that reads app credentials and access tokens from a JSON file pointed to by LARKSUITE_CLI_CREDENTIAL_FILE, targeting non-interactive CI/CD use cases. The validation logic, BlockError handling, and test coverage are thorough, but the token-refresh path has three P1 reliability bugs that should be addressed before merging.

  • Context dropped: The context.Context passed to ResolveToken is not threaded into refreshUAT, so the HTTP request for token refresh is not cancellable.
  • Stale cache after credential rotation: refreshCache is not keyed on the refresh_token value; rotating credentials in the file will not invalidate the in-process cache for up to 2 hours.
  • No HTTP timeout: http.DefaultClient is used without a Timeout, causing the process to hang indefinitely if the token endpoint is slow or unreachable.

Confidence Score: 3/5

Not safe to merge as-is: three P1 bugs in the refresh token path can cause indefinite hangs, stale credentials after rotation, and unresponsive behaviour on context cancellation.

All three P1 findings are in the refreshUAT code path: context not propagated (breaking the Provider contract), cache not invalidated on credential rotation (silent stale data), and no HTTP timeout (indefinite hang in CI/CD). These are in a specific code path (refresh token flow), but since it is a primary feature of the new provider, they warrant a score of 3.

extension/credential/jsonfile/jsonfile.go — specifically the refreshUAT function and the refreshCache global variable.

Vulnerabilities

  • Credentials (including app_secret and refresh_token) are stored in a plaintext JSON file; the implementation correctly enforces absolute-path-only and rejects control characters, but does not check or enforce restrictive file permissions (e.g., mode 0600) beyond what the user provides.
  • No injection vectors identified: path is cleaned with filepath.Clean and validated as absolute before os.ReadFile.
  • The token refresh HTTP request uses form-encoded parameters (no raw interpolation) and response parsing is done with json.Unmarshal, so no injection risk there.

Important Files Changed

Filename Overview
extension/credential/jsonfile/jsonfile.go New JSON file credential provider: 3 P1 issues — context dropped before HTTP call, refresh cache not keyed on the token value (stale cache after rotation), and http.DefaultClient used without a timeout.
extension/credential/jsonfile/jsonfile_test.go 27 well-structured tests covering happy paths, validation errors, cache hits, and refresh token flows via an httptest server; no issues found.
internal/envvars/envvars.go Adds LARKSUITE_CLI_CREDENTIAL_FILE constant; straightforward and correct.
main.go Adds blank import to activate the jsonfile provider on startup; correct.

Sequence Diagram

sequenceDiagram
    participant CLI as lark-cli
    participant Reg as credential.Registry
    participant JP as jsonfile.Provider
    participant FS as File System
    participant OAuth as OAuth Token Endpoint

    CLI->>Reg: ResolveAccount(ctx)
    Reg->>JP: ResolveAccount(ctx)
    JP->>FS: os.Getenv(LARKSUITE_CLI_CREDENTIAL_FILE)
    JP->>FS: os.ReadFile(path)
    FS-->>JP: credFile JSON
    JP-->>Reg: *credential.Account

    CLI->>Reg: ResolveToken(ctx, UAT)
    Reg->>JP: ResolveToken(ctx, UAT)
    JP->>FS: os.ReadFile(path)
    FS-->>JP: credFile JSON
    alt user_access_token set
        JP-->>Reg: Token{Value: uat}
    else refresh_token set
        JP->>JP: refreshUAT(cf) [no ctx!]
        alt cache hit
            JP-->>Reg: Token{cached}
        else cache miss
            JP->>OAuth: POST /oauth/token (refresh_token grant)
            OAuth-->>JP: access_token + expires_in
            JP-->>Reg: Token{refreshed}
        end
    end
Loading

Reviews (1): Last reviewed commit: "feat: add jsonfile credential provider f..." | Re-trigger Greptile

Comment on lines +182 to +204
func refreshUAT(cf *credFile) (string, error) {
refreshMu.Lock()
defer refreshMu.Unlock()

if refreshCache != nil && time.Now().Before(refreshCache.expiresAt.Add(-refreshAheadDuration)) {
return refreshCache.value, nil
}

tokenURL := tokenEndpointFunc(cf.Brand)

form := url.Values{}
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", cf.RefreshToken)
form.Set("client_id", cf.AppID)
form.Set("client_secret", cf.AppSecret)

req, err := http.NewRequest("POST", tokenURL, strings.NewReader(form.Encode()))
if err != nil {
return "", &credential.BlockError{Provider: "jsonfile", Reason: fmt.Sprintf("failed to create refresh request: %v", err)}
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := http.DefaultClient.Do(req)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Context not propagated to HTTP refresh request

The ctx received by ResolveToken is dropped before reaching refreshUAT. The HTTP request is created with http.NewRequest rather than http.NewRequestWithContext, so caller-initiated cancellations (Ctrl-C, deadline) are silently ignored and the network call can run indefinitely.

Suggested change
func refreshUAT(cf *credFile) (string, error) {
refreshMu.Lock()
defer refreshMu.Unlock()
if refreshCache != nil && time.Now().Before(refreshCache.expiresAt.Add(-refreshAheadDuration)) {
return refreshCache.value, nil
}
tokenURL := tokenEndpointFunc(cf.Brand)
form := url.Values{}
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", cf.RefreshToken)
form.Set("client_id", cf.AppID)
form.Set("client_secret", cf.AppSecret)
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(form.Encode()))
if err != nil {
return "", &credential.BlockError{Provider: "jsonfile", Reason: fmt.Sprintf("failed to create refresh request: %v", err)}
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
resp, err := http.DefaultClient.Do(req)
func refreshUAT(ctx context.Context, cf *credFile) (string, error) {
refreshMu.Lock()
defer refreshMu.Unlock()
if refreshCache != nil && time.Now().Before(refreshCache.expiresAt.Add(-refreshAheadDuration)) {
return refreshCache.value, nil
}
tokenURL := tokenEndpointFunc(cf.Brand)
form := url.Values{}
form.Set("grant_type", "refresh_token")
form.Set("refresh_token", cf.RefreshToken)
form.Set("client_id", cf.AppID)
form.Set("client_secret", cf.AppSecret)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, tokenURL, strings.NewReader(form.Encode()))

You'll also need to update resolveUAT to accept and thread ctx through, and update the call-site in ResolveToken.

}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := http.DefaultClient.Do(req)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 http.DefaultClient has no timeout

http.DefaultClient has no Timeout set, so if the OAuth token endpoint is slow or unreachable the CLI will hang indefinitely. This is especially impactful for CI/CD pipelines where a stuck process blocks the pipeline.

Suggested change
resp, err := http.DefaultClient.Do(req)
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)

Comment on lines +41 to +46
var (
refreshMu sync.Mutex
refreshCache *cachedToken

tokenEndpointFunc = defaultTokenEndpoint
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 refreshCache not keyed on refresh_token value

The in-process cache stores the last fetched access token but never records which refresh_token was used to obtain it. In CI/CD environments (the primary use case), credential rotation is common: if the file is updated with a new refresh_token, refreshUAT will keep returning the stale cached token for up to 2 hours rather than fetching a new one with the updated value.

The cachedToken struct should store the input token used to obtain it, and the cache-hit check should compare against cf.RefreshToken:

type cachedToken struct {
    value        string
    expiresAt    time.Time
    refreshToken string // invalidate when input changes
}

// In refreshUAT, before returning cached value:
if refreshCache != nil &&
    refreshCache.refreshToken == cf.RefreshToken &&
    time.Now().Before(refreshCache.expiresAt.Add(-refreshAheadDuration)) {
    return refreshCache.value, nil
}

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
README.md (1)

199-207: Missing default_as field in JSON example.

The JSON example omits the default_as field, but the implementation in jsonfile.go supports it (and tests verify it works). The fields table below mentions it should be documented to match the implementation's capabilities.

Consider adding "default_as": "user" to the example for completeness, or add a note that this optional field can be included.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 199 - 207, The README's JSON example is missing the
optional "default_as" field that jsonfile.go supports; update the example JSON
to include "default_as": "user" (or add a short note that this optional field is
supported) so the docs match the implementation and tests referencing
jsonfile.go's handling of default_as.
extension/credential/jsonfile/jsonfile_test.go (2)

116-119: Consider checking error in TestResolveAccount_DefaultBrand.

The error is ignored on line 116. If ResolveAccount fails, acct will be nil and the test will panic with a less informative message than an explicit error check would provide.

Proposed fix
-	acct, _ := (&Provider{}).ResolveAccount(context.Background())
+	acct, err := (&Provider{}).ResolveAccount(context.Background())
+	if err != nil {
+		t.Fatal(err)
+	}
 	if acct.Brand != "feishu" {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/credential/jsonfile/jsonfile_test.go` around lines 116 - 119, The
test TestResolveAccount_DefaultBrand ignores the error returned by
(&Provider{}).ResolveAccount which can lead to a nil acct and a panic; modify
the test to check the returned error from ResolveAccount (store both acct and
err), fail the test with t.Fatalf or t.Fatalf-like message if err != nil, then
continue to assert acct.Brand == "feishu" to ensure a clear failure message when
ResolveAccount fails; reference the Provider type, ResolveAccount method, and
acct variable in your change.

22-29: Consider checking error from os.WriteFile.

The error from os.WriteFile is silently ignored. While test failures would likely surface as subsequent errors, explicitly checking would make debugging easier if file creation fails.

Proposed fix
 func writeTempJSON(t *testing.T, data map[string]string) string {
 	t.Helper()
 	dir := t.TempDir()
 	path := filepath.Join(dir, "cred.json")
 	b, _ := json.Marshal(data)
-	os.WriteFile(path, b, 0o600)
+	if err := os.WriteFile(path, b, 0o600); err != nil {
+		t.Fatalf("failed to write test credential file: %v", err)
+	}
 	return path
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/credential/jsonfile/jsonfile_test.go` around lines 22 - 29, In
writeTempJSON(t *testing.T, data map[string]string) ensure you check and handle
the error returned by os.WriteFile instead of ignoring it: capture the error
(and optionally json.Marshal error) and call t.Fatalf or t.Fatalf with a clear
message including the error and path so the test fails immediately if writing
the temp file fails; reference the writeTempJSON function and the os.WriteFile
call to locate where to add the error check.
README.zh.md (1)

200-208: Missing default_as field in JSON example (same as English README).

For consistency with the implementation, consider adding the default_as field to both the JSON example and the fields table.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.zh.md` around lines 200 - 208, The JSON example shown (the snippet
containing keys "app_id", "app_secret", "brand", "user_access_token",
"tenant_access_token", "refresh_token") is missing the required "default_as"
field; add "default_as" to that JSON example with an appropriate sample value
and also update the corresponding fields table in the README.zh.md to include a
row describing "default_as" (its purpose and expected values) so the Chinese
README matches the English version and the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@extension/credential/jsonfile/jsonfile.go`:
- Around line 198-207: The code creates an HTTP request with http.NewRequest and
calls http.DefaultClient.Do(req) (around the token refresh logic using tokenURL
and form.Encode()) without a timeout, which can block indefinitely; change this
to use a context with timeout (e.g., context.WithTimeout) and attach it to the
request via req = req.WithContext(ctx) (or use a custom http.Client with
Timeout) and ensure you call cancel() after the request; keep the existing error
handling that returns &credential.BlockError but wrap/report context
deadline/cancel errors appropriately.
- Around line 259-262: Replace the os.ReadFile call with vfs.ReadFile in the
credential loading logic (the snippet that reads into data using variable
cleaned) and update imports to include "github.com/larksuite/cli/internal/vfs";
keep the existing error handling that returns &credential.BlockError{Provider:
"jsonfile", Reason: fmt.Sprintf(...)} but use the error returned by vfs.ReadFile
so behavior is unchanged except using the vfs layer for filesystem access.

---

Nitpick comments:
In `@extension/credential/jsonfile/jsonfile_test.go`:
- Around line 116-119: The test TestResolveAccount_DefaultBrand ignores the
error returned by (&Provider{}).ResolveAccount which can lead to a nil acct and
a panic; modify the test to check the returned error from ResolveAccount (store
both acct and err), fail the test with t.Fatalf or t.Fatalf-like message if err
!= nil, then continue to assert acct.Brand == "feishu" to ensure a clear failure
message when ResolveAccount fails; reference the Provider type, ResolveAccount
method, and acct variable in your change.
- Around line 22-29: In writeTempJSON(t *testing.T, data map[string]string)
ensure you check and handle the error returned by os.WriteFile instead of
ignoring it: capture the error (and optionally json.Marshal error) and call
t.Fatalf or t.Fatalf with a clear message including the error and path so the
test fails immediately if writing the temp file fails; reference the
writeTempJSON function and the os.WriteFile call to locate where to add the
error check.

In `@README.md`:
- Around line 199-207: The README's JSON example is missing the optional
"default_as" field that jsonfile.go supports; update the example JSON to include
"default_as": "user" (or add a short note that this optional field is supported)
so the docs match the implementation and tests referencing jsonfile.go's
handling of default_as.

In `@README.zh.md`:
- Around line 200-208: The JSON example shown (the snippet containing keys
"app_id", "app_secret", "brand", "user_access_token", "tenant_access_token",
"refresh_token") is missing the required "default_as" field; add "default_as" to
that JSON example with an appropriate sample value and also update the
corresponding fields table in the README.zh.md to include a row describing
"default_as" (its purpose and expected values) so the Chinese README matches the
English version and the implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: b42a1ac1-62aa-4c3d-a5ba-265048d4c468

📥 Commits

Reviewing files that changed from the base of the PR and between 0bf4f80 and 116cfa0.

📒 Files selected for processing (6)
  • README.md
  • README.zh.md
  • extension/credential/jsonfile/jsonfile.go
  • extension/credential/jsonfile/jsonfile_test.go
  • internal/envvars/envvars.go
  • main.go

Comment on lines +198 to +207
req, err := http.NewRequest("POST", tokenURL, strings.NewReader(form.Encode()))
if err != nil {
return "", &credential.BlockError{Provider: "jsonfile", Reason: fmt.Sprintf("failed to create refresh request: %v", err)}
}
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")

resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", &credential.BlockError{Provider: "jsonfile", Reason: fmt.Sprintf("refresh token request failed: %v", err)}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add timeout to HTTP request to prevent indefinite blocking.

The HTTP request to the OAuth token endpoint uses http.DefaultClient without a timeout. If the server is slow or unresponsive, this could block indefinitely. Consider using a context with timeout or a custom http.Client.

Proposed fix using context timeout
-func refreshUAT(cf *credFile) (string, error) {
+func refreshUAT(ctx context.Context, cf *credFile) (string, error) {
 	refreshMu.Lock()
 	defer refreshMu.Unlock()

 	if refreshCache != nil && time.Now().Before(refreshCache.expiresAt.Add(-refreshAheadDuration)) {
 		return refreshCache.value, nil
 	}

 	tokenURL := tokenEndpointFunc(cf.Brand)

 	form := url.Values{}
 	form.Set("grant_type", "refresh_token")
 	form.Set("refresh_token", cf.RefreshToken)
 	form.Set("client_id", cf.AppID)
 	form.Set("client_secret", cf.AppSecret)

-	req, err := http.NewRequest("POST", tokenURL, strings.NewReader(form.Encode()))
+	ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
+	defer cancel()
+	req, err := http.NewRequestWithContext(ctx, "POST", tokenURL, strings.NewReader(form.Encode()))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/credential/jsonfile/jsonfile.go` around lines 198 - 207, The code
creates an HTTP request with http.NewRequest and calls
http.DefaultClient.Do(req) (around the token refresh logic using tokenURL and
form.Encode()) without a timeout, which can block indefinitely; change this to
use a context with timeout (e.g., context.WithTimeout) and attach it to the
request via req = req.WithContext(ctx) (or use a custom http.Client with
Timeout) and ensure you call cancel() after the request; keep the existing error
handling that returns &credential.BlockError but wrap/report context
deadline/cancel errors appropriately.

Comment on lines +259 to +262
data, err := os.ReadFile(cleaned)
if err != nil {
return nil, &credential.BlockError{Provider: "jsonfile", Reason: fmt.Sprintf("cannot read credential file %s: %v", cleaned, err)}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for vfs usage patterns in the codebase
rg -n "vfs\.(Read|Write|Open)" --type=go -C2

# Check if vfs package exists
fd -t f "vfs.go" --exec head -30 {}

Repository: larksuite/cli

Length of output: 11049


🏁 Script executed:

# Examine the jsonfile.go file
cat -n extension/credential/jsonfile/jsonfile.go | head -270 | tail -50

Repository: larksuite/cli

Length of output: 2174


🏁 Script executed:

# Check imports and overall structure
head -50 extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 1127


🏁 Script executed:

# Check for all os.* and vfs.* usage in the file
rg -n "os\.|vfs\." extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 202


🏁 Script executed:

# Check what validate.SafeInputPath looks like if present
rg -n "SafeInputPath|validate\." extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check how validate.SafeInputPath is used in the codebase
rg -n "SafeInputPath" --type=go | head -20

Repository: larksuite/cli

Length of output: 1912


🏁 Script executed:

# Check if vfs is imported anywhere and the pattern
rg -n "import.*vfs" --type=go | head -10

Repository: larksuite/cli

Length of output: 224


🏁 Script executed:

# Look at examples of path validation with vfs in the codebase
rg -B3 "vfs.ReadFile" --type=go | head -40

Repository: larksuite/cli

Length of output: 2076


🏁 Script executed:

# Check what validate module exports
head -50 internal/validate/path.go

Repository: larksuite/cli

Length of output: 1078


🏁 Script executed:

# Confirm jsonfile.go is production code (not a test file)
file extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 101


🏁 Script executed:

# Check if there are any special exceptions for credential files
rg -n "credential" AGENTS.md

Repository: larksuite/cli

Length of output: 319


🏁 Script executed:

# Double-check the imports in jsonfile.go to see if vfs or validate are imported
grep -n "^import\|vfs\|validate" extension/credential/jsonfile/jsonfile.go | head -30

Repository: larksuite/cli

Length of output: 67


🏁 Script executed:

# Look at how SafeEnvDirPath works since this is reading from an env-provided path
rg -A10 "SafeEnvDirPath" internal/vfs/localfileio/path.go | head -30

Repository: larksuite/cli

Length of output: 619


🏁 Script executed:

# Check all filesystem-related os.* calls in jsonfile.go
rg -n "os\.(Read|Write|Open|Create|Mkdir|Remove|Stat)" extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 95


🏁 Script executed:

# Verify the pattern by looking at a complete example in another credential/auth file
fd -t f "auth.*\.go" cmd/ internal/ | head -5 | xargs grep -l "SafeEnvDirPath\|SafeInputPath" 2>/dev/null

Repository: larksuite/cli

Length of output: 86


🏁 Script executed:

# Look at a complete example from internal/core/config.go which also reads config
grep -B5 -A5 "vfs.ReadFile(GetConfigPath())" internal/core/config.go

Repository: larksuite/cli

Length of output: 330


🏁 Script executed:

# Check what imports are already in jsonfile.go
sed -n '6,22p' extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 369


🏁 Script executed:

# Check if there are other credential providers that use similar patterns
fd -t f "credential.*\.go" extension/ --exec head -30 {} \; 2>/dev/null | grep -A5 "import"

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Verify the exact path source - it comes from an env var
grep -n "CliCredentialFile" extension/credential/jsonfile/jsonfile.go

Repository: larksuite/cli

Length of output: 364


🏁 Script executed:

# Check what SafeInputPath validation includes vs current manual validation
rg -A20 "func SafeInputPath" internal/vfs/localfileio/path.go

Repository: larksuite/cli

Length of output: 951


🏁 Script executed:

# Confirm vfs is properly exported and available to import
head -20 internal/vfs/vfs.go

Repository: larksuite/cli

Length of output: 135


🏁 Script executed:

# List what's in the vfs package
ls -la internal/vfs/

Repository: larksuite/cli

Length of output: 470


🏁 Script executed:

# Find the vfs package entry point
fd "vfs" internal/ --type f | head -10

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check if vfs is imported from internal/core as shown in config.go
grep -n "import.*vfs" internal/core/config.go

Repository: larksuite/cli

Length of output: 39


🏁 Script executed:

# Check the vfs package interface
cat internal/vfs/fs.go

Repository: larksuite/cli

Length of output: 959


🏁 Script executed:

# Check the default implementation
cat internal/vfs/default.go

Repository: larksuite/cli

Length of output: 1600


🏁 Script executed:

# Check how config.go imports vfs
grep -B2 -A10 "import" internal/core/config.go | head -30

Repository: larksuite/cli

Length of output: 254


🏁 Script executed:

# Check how config.go actually imports and uses vfs
grep -n "vfs" internal/core/config.go

Repository: larksuite/cli

Length of output: 237


🏁 Script executed:

# Verify the pattern - does config.go use validate.SafeInputPath or direct vfs?
sed -n '185,195p' internal/core/config.go

Repository: larksuite/cli

Length of output: 390


🏁 Script executed:

# Check what GetConfigPath returns to understand the full flow
grep -B5 "func GetConfigPath" internal/core/config.go

Repository: larksuite/cli

Length of output: 180


Replace os.ReadFile with vfs.ReadFile to comply with filesystem access guidelines.

Line 259 must use vfs.ReadFile instead of os.ReadFile per the coding guidelines for all filesystem operations in production code. This ensures testability and consistent behavior across the codebase. Add "github.com/larksuite/cli/internal/vfs" to imports and replace the call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@extension/credential/jsonfile/jsonfile.go` around lines 259 - 262, Replace
the os.ReadFile call with vfs.ReadFile in the credential loading logic (the
snippet that reads into data using variable cleaned) and update imports to
include "github.com/larksuite/cli/internal/vfs"; keep the existing error
handling that returns &credential.BlockError{Provider: "jsonfile", Reason:
fmt.Sprintf(...)} but use the error returned by vfs.ReadFile so behavior is
unchanged except using the vfs layer for filesystem access.

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

Labels

size/M Single-domain feat or fix with limited business impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants