-
Notifications
You must be signed in to change notification settings - Fork 1.3k
fix(auth): make agent authorization resumable #1867
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -6,7 +6,9 @@ package auth | |||||||||||||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||||||||||||||
| "context" | ||||||||||||||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||
| "sort" | ||||||||||||||||||||||||||||||||||||||||||||||
| "strings" | ||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -36,6 +38,7 @@ type LoginOptions struct { | |||||||||||||||||||||||||||||||||||||||||||||
| Exclude []string | ||||||||||||||||||||||||||||||||||||||||||||||
| NoWait bool | ||||||||||||||||||||||||||||||||||||||||||||||
| DeviceCode string | ||||||||||||||||||||||||||||||||||||||||||||||
| Resume bool | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| var pollDeviceToken = larkauth.PollDeviceToken | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -52,7 +55,7 @@ func NewCmdAuthLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra. | |||||||||||||||||||||||||||||||||||||||||||||
| For AI agents: this command blocks until the user completes authorization in the | ||||||||||||||||||||||||||||||||||||||||||||||
| browser. If your harness or agent tool only delivers final turn messages, use --no-wait --json, | ||||||||||||||||||||||||||||||||||||||||||||||
| send the verification URL (or QR code) to the user as your final message, end the turn, then | ||||||||||||||||||||||||||||||||||||||||||||||
| run --device-code in a later step after the user confirms authorization. Use 'lark-cli auth qrcode' | ||||||||||||||||||||||||||||||||||||||||||||||
| run --resume in a later step after the user confirms authorization. Use 'lark-cli auth qrcode' | ||||||||||||||||||||||||||||||||||||||||||||||
| to generate QR codes (supports ASCII and PNG formats).`, | ||||||||||||||||||||||||||||||||||||||||||||||
| RunE: func(cmd *cobra.Command, args []string) error { | ||||||||||||||||||||||||||||||||||||||||||||||
| if mode := f.ResolveStrictMode(cmd.Context()); mode == core.StrictModeBot { | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -84,8 +87,9 @@ to generate QR codes (supports ASCII and PNG formats).`, | |||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().StringSliceVar(&opts.Exclude, "exclude", nil, | ||||||||||||||||||||||||||||||||||||||||||||||
| "scopes to exclude from the request (repeatable or comma-separated, e.g. --exclude drive:file:download)") | ||||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().BoolVar(&opts.JSON, "json", false, "structured JSON output") | ||||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().BoolVar(&opts.NoWait, "no-wait", false, "initiate device authorization and return immediately; use --device-code to complete") | ||||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().BoolVar(&opts.NoWait, "no-wait", false, "initiate device authorization and return immediately; use --resume to complete later") | ||||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().StringVar(&opts.DeviceCode, "device-code", "", "poll and complete authorization with a device code from a previous --no-wait call") | ||||||||||||||||||||||||||||||||||||||||||||||
| cmd.Flags().BoolVar(&opts.Resume, "resume", false, "resume the latest pending authorization created by --no-wait") | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| cmdutil.RegisterFlagCompletion(cmd, "domain", func(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return completeDomain(toComplete), cobra.ShellCompDirectiveNoFileComp | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -132,6 +136,21 @@ func authLoginRun(opts *LoginOptions) error { | |||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| msg := getLoginMsg(lang) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| if opts.Resume { | ||||||||||||||||||||||||||||||||||||||||||||||
| if opts.DeviceCode != "" || opts.NoWait || opts.Scope != "" || opts.Recommend || len(opts.Domains) > 0 || len(opts.Exclude) > 0 { | ||||||||||||||||||||||||||||||||||||||||||||||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "--resume cannot be combined with authorization request options or --device-code").WithParam("--resume") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| pending, err := loadPendingLogin(config.AppID) | ||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| if errors.Is(err, os.ErrNotExist) { | ||||||||||||||||||||||||||||||||||||||||||||||
| return errs.NewAuthenticationError(errs.SubtypeUnknown, "no unexpired pending authorization to resume"). | ||||||||||||||||||||||||||||||||||||||||||||||
| WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| return errs.NewInternalError(errs.SubtypeStorage, "failed to load pending authorization: %v", err).WithCause(err) | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+139
to
+149
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Classify missing pending state as a failed precondition.
As per coding guidelines, valid requests made in the wrong system state must use Proposed fix- return errs.NewAuthenticationError(errs.SubtypeUnknown, "no unexpired pending authorization to resume").
+ return errs.NewValidationError(errs.SubtypeFailedPrecondition, "no unexpired pending authorization to resume").
WithHint("start a fresh split flow with `lark-cli auth login --scope <scope> --no-wait --json`, send its verification URL to the user, then run `lark-cli auth login --resume` only after the user confirms authorization")📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| opts.DeviceCode = pending.DeviceCode | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| log := func(format string, a ...interface{}) { | ||||||||||||||||||||||||||||||||||||||||||||||
| if !opts.JSON { | ||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(f.IOStreams.ErrOut, format+"\n", a...) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -200,7 +219,7 @@ func authLoginRun(opts *LoginOptions) error { | |||||||||||||||||||||||||||||||||||||||||||||
| log("View all options:") | ||||||||||||||||||||||||||||||||||||||||||||||
| log(msg.HintFooter) | ||||||||||||||||||||||||||||||||||||||||||||||
| log("") | ||||||||||||||||||||||||||||||||||||||||||||||
| log("Note: this command blocks until authorization is complete. For non-streaming agent harnesses, use --no-wait --json, send the verification URL as the final message of the turn, then run --device-code in a later step after the user confirms authorization.") | ||||||||||||||||||||||||||||||||||||||||||||||
| log("Note: this command blocks until authorization is complete. For non-streaming agent harnesses, use --no-wait --json, send the verification URL as the final message of the turn, then run --resume in a later step after the user confirms authorization.") | ||||||||||||||||||||||||||||||||||||||||||||||
| return errs.NewValidationError(errs.SubtypeInvalidArgument, "please specify the scopes to authorize").WithParam("--scope") | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -272,7 +291,7 @@ func authLoginRun(opts *LoginOptions) error { | |||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| // --no-wait: return immediately with device code and URL | ||||||||||||||||||||||||||||||||||||||||||||||
| if opts.NoWait { | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := saveLoginRequestedScope(authResp.DeviceCode, finalScope); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+294
to
296
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Fail The command currently returns success and instructs the agent to use Proposed fix if opts.NoWait {
if err := savePendingLogin(authResp.DeviceCode, config.AppID, finalScope, authResp.ExpiresIn); err != nil {
- fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to cache requested scopes: %v\n", err)
+ return errs.NewInternalError(errs.SubtypeStorage,
+ "failed to save pending authorization: %v", err).WithCause(err)
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI AgentsSource: Coding guidelines |
||||||||||||||||||||||||||||||||||||||||||||||
| data := map[string]interface{}{ | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -283,9 +302,9 @@ func authLoginRun(opts *LoginOptions) error { | |||||||||||||||||||||||||||||||||||||||||||||
| "**CRITICAL: You MUST include the QR image in your response.** Generating the file alone is NOT enough—use image tags, inline images, or file attachments to display it." + | ||||||||||||||||||||||||||||||||||||||||||||||
| "**Display order:** Output the URL first, then place the QR code image below the URL." + | ||||||||||||||||||||||||||||||||||||||||||||||
| "**URL Output Rules:** Treat verification_url as an opaque string that cannot be modified. Do NOT URL-encode/decode or add spaces/punctuation." + | ||||||||||||||||||||||||||||||||||||||||||||||
| "For agent harnesses that only deliver final turn messages, make the QR code image (or URL) the final message of the turn and return control to the user; do not block on --device-code in the same turn. **Before ending the turn, tell the user to come back and notify you after completing authorization.**" + | ||||||||||||||||||||||||||||||||||||||||||||||
| "**After the user confirms authorization:** YOU must execute `lark-cli auth login --device-code <device_code>` yourself." + | ||||||||||||||||||||||||||||||||||||||||||||||
| "**Do NOT cache verification_url or device_code for future use.** Always run `lark-cli auth login --no-wait --json` fresh when authorization is needed.", | ||||||||||||||||||||||||||||||||||||||||||||||
| "For agent harnesses that only deliver final turn messages, make the QR code image (or URL) the final message of the turn and return control to the user; do not run --resume in the same turn. **Before ending the turn, tell the user to come back and notify you after completing authorization.**" + | ||||||||||||||||||||||||||||||||||||||||||||||
| "**After the user confirms authorization:** YOU must execute `lark-cli auth login --resume` yourself. The CLI keeps the pending device code locally; do not copy it into a task comment or ask the user to run a command." + | ||||||||||||||||||||||||||||||||||||||||||||||
| "If --resume reports that the pending authorization expired, start a fresh flow with `lark-cli auth login --no-wait --json` and show the new URL.", | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| encoder := json.NewEncoder(f.IOStreams.Out) | ||||||||||||||||||||||||||||||||||||||||||||||
| encoder.SetEscapeHTML(false) | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -403,7 +422,7 @@ func authLoginPollDeviceCode(opts *LoginOptions, config *core.CliConfig, msg *lo | |||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to load cached requested scopes: %v\n", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| cleanupRequestedScope := func() { | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := removeLoginRequestedScope(opts.DeviceCode); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| if err := removePendingLogin(opts.DeviceCode, config.AppID); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||
| fmt.Fprintf(f.IOStreams.ErrOut, "[lark-cli] [WARN] auth login: failed to remove cached requested scopes: %v\n", err) | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -9,6 +9,7 @@ import ( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "os" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "path/filepath" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "regexp" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "time" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| larkauth "github.com/larksuite/cli/internal/auth" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "github.com/larksuite/cli/internal/core" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -22,6 +23,13 @@ type loginScopeCacheRecord struct { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RequestedScope string `json:"requested_scope"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type pendingLoginRecord struct { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DeviceCode string `json:"device_code"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AppID string `json:"app_id"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RequestedScope string `json:"requested_scope"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExpiresAt int64 `json:"expires_at"` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // loginScopeCacheDir returns the directory used to persist auth login --no-wait | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // requested scopes keyed by device_code. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func loginScopeCacheDir() string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -33,6 +41,10 @@ func loginScopeCachePath(deviceCode string) string { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return filepath.Join(loginScopeCacheDir(), sanitizeLoginScopeCacheKey(deviceCode)+".json") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func pendingLoginPath(appID string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return filepath.Join(loginScopeCacheDir(), "latest-"+sanitizeLoginScopeCacheKey(appID)+".json") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // sanitizeLoginScopeCacheKey converts a device_code into a safe filename token. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func sanitizeLoginScopeCacheKey(deviceCode string) string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sanitized := loginScopeCacheSafeChars.ReplaceAllString(deviceCode, "_") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -54,6 +66,45 @@ func saveLoginRequestedScope(deviceCode, requestedScope string) error { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return validate.AtomicWrite(loginScopeCachePath(deviceCode), data, 0600) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // savePendingLogin persists the latest split-flow authorization so a later | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // agent turn can resume it without copying a device code through the model's | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // conversation context. The per-device scope record remains for backwards | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // compatibility with the explicit --device-code flow. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func savePendingLogin(deviceCode, appID, requestedScope string, expiresIn int) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := saveLoginRequestedScope(deviceCode, requestedScope); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| record := pendingLoginRecord{ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DeviceCode: deviceCode, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| AppID: appID, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| RequestedScope: requestedScope, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ExpiresAt: time.Now().Add(time.Duration(expiresIn) * time.Second).Unix(), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data, err := json.Marshal(record) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return validate.AtomicWrite(pendingLoginPath(appID), data, 0600) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func loadPendingLogin(appID string) (*pendingLoginRecord, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path := pendingLoginPath(appID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data, err := vfs.ReadFile(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var record pendingLoginRecord | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := json.Unmarshal(data, &record); err != nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = vfs.Remove(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if record.DeviceCode == "" || record.AppID != appID || record.ExpiresAt <= time.Now().Unix() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| _ = vfs.Remove(path) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return nil, os.ErrNotExist | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+101
to
+103
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Remove the expired flow’s per-device scope record too. Expiration deletes only Proposed fix- if record.DeviceCode == "" || record.AppID != appID || record.ExpiresAt <= time.Now().Unix() {
+ if record.DeviceCode == "" || record.AppID != appID {
_ = vfs.Remove(path)
return nil, os.ErrNotExist
}
+ if record.ExpiresAt <= time.Now().Unix() {
+ _ = removeLoginRequestedScope(record.DeviceCode)
+ _ = vfs.Remove(path)
+ return nil, os.ErrNotExist
+ }📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return &record, nil | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // loadLoginRequestedScope loads the cached requested scope string for a device_code. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // It returns an empty string if no cache entry exists. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func loadLoginRequestedScope(deviceCode string) (string, error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -81,6 +132,20 @@ func removeLoginRequestedScope(deviceCode string) error { | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func removePendingLogin(deviceCode, appID string) error { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstErr := removeLoginRequestedScope(deviceCode) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path := pendingLoginPath(appID) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if data, err := vfs.ReadFile(path); err == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var record pendingLoginRecord | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if err := vfs.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) && firstErr == nil { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| firstErr = err | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return firstErr | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+135
to
+146
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win Do not silently ignore pending-file read failures. If Proposed fix path := pendingLoginPath(appID)
- if data, err := vfs.ReadFile(path); err == nil {
+ data, err := vfs.ReadFile(path)
+ if err != nil {
+ if !errors.Is(err, os.ErrNotExist) && firstErr == nil {
+ firstErr = err
+ }
+ return firstErr
+ }
+ {
var record pendingLoginRecord
if json.Unmarshal(data, &record) == nil && record.DeviceCode == deviceCode {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // shouldRemoveLoginRequestedScope indicates whether the requested-scope cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // should be removed after polling finishes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| func shouldRemoveLoginRequestedScope(result *larkauth.DeviceFlowResult) bool { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
Add direct parsing coverage for
--resume.The new Cobra flag is not exercised by the supplied flag-parsing test. Add a command-level assertion that
--resumesetsLoginOptions.Resume.As per coding guidelines, every behavior change needs a test alongside the change.
🤖 Prompt for AI Agents
Source: Coding guidelines