mirror of
https://github.com/larksuite/cli.git
synced 2026-07-03 14:02:43 +08:00
603 lines
20 KiB
Go
603 lines
20 KiB
Go
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package apps
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"text/tabwriter"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
|
"github.com/spf13/cobra"
|
|
|
|
"github.com/larksuite/cli/errs"
|
|
"github.com/larksuite/cli/internal/client"
|
|
"github.com/larksuite/cli/internal/cmdutil"
|
|
"github.com/larksuite/cli/internal/core"
|
|
"github.com/larksuite/cli/internal/errclass"
|
|
"github.com/larksuite/cli/internal/keychain"
|
|
"github.com/larksuite/cli/internal/validate"
|
|
"github.com/larksuite/cli/shortcuts/apps/gitcred"
|
|
"github.com/larksuite/cli/shortcuts/common"
|
|
)
|
|
|
|
const gitCredentialIssuePath = apiBasePath + "/apps/:app_id/git_info"
|
|
const gitCredentialHelperReportedShortcut = appsService + ":+git-credential-helper"
|
|
|
|
// gitCredentialIssueHint is the actionable next-step attached to a failed
|
|
// Git-credential issuance. A 5xx is flagged retryable separately at the call site.
|
|
const gitCredentialIssueHint = "failed to issue the Git credential: verify --app-id is correct and you have developer access to this app; a 5xx is a transient server error and is safe to retry"
|
|
|
|
var AppsGitCredentialInit = common.Shortcut{
|
|
Service: appsService,
|
|
Command: "+git-credential-init",
|
|
Description: "Initialize Git credentials and a URL-scoped Git helper for an app repository",
|
|
Risk: "write",
|
|
Tips: []string{
|
|
"Example: lark-cli apps +git-credential-init --app-id <app_id>",
|
|
},
|
|
Scopes: []string{"spark:app:read"},
|
|
AuthTypes: []string{"user"},
|
|
HasFormat: true,
|
|
Flags: []common.Flag{
|
|
{Name: "app-id", Desc: "app ID", Required: true},
|
|
},
|
|
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
|
if strings.TrimSpace(rctx.Str("app-id")) == "" {
|
|
return appsValidationParamError("--app-id", "--app-id is required")
|
|
}
|
|
if err := validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id"); err != nil {
|
|
return appsValidationParamError("--app-id", "%v", err).WithCause(err)
|
|
}
|
|
return nil
|
|
},
|
|
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
|
appID := strings.TrimSpace(rctx.Str("app-id"))
|
|
return common.NewDryRunAPI().
|
|
GET(gitCredentialIssuePath).
|
|
Desc("Issue an app Git repository PAT").
|
|
Set("mode", "api-plus-local-setup").
|
|
Set("action", "initialize_local_git_credential").
|
|
Set("app_id", appID).
|
|
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
|
|
Set("local_effects", []string{
|
|
"save the issued PAT in the local system credential store",
|
|
"write app-scoped git credential metadata",
|
|
"configure a URL-scoped Git credential helper in global git config when possible",
|
|
}).
|
|
Params(gitCredentialIssueParams(appID))
|
|
},
|
|
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
|
appID := strings.TrimSpace(rctx.Str("app-id"))
|
|
manager := newGitCredentialManager(appID, rctx.Factory.Keychain, runtimeIssuer{rctx: rctx})
|
|
result, err := manager.Init(ctx, profileFromConfig(rctx.Config), appID)
|
|
if err != nil {
|
|
return gitCredentialLocalError("Initialize local app Git credential", err)
|
|
}
|
|
payload := map[string]interface{}{
|
|
"app_id": result.AppID,
|
|
"repository_url": result.GitHTTPURL,
|
|
"status": initStatus(result),
|
|
}
|
|
if result.ConfigWarning != "" {
|
|
payload["git_config_warning"] = result.ConfigWarning
|
|
}
|
|
rctx.OutFormat(payload, nil, func(w io.Writer) {
|
|
title := "Git credential initialized"
|
|
if result.Refreshed {
|
|
title = "Git credential refreshed"
|
|
}
|
|
fmt.Fprintln(w, title)
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintf(w, "App ID: %s\n", result.AppID)
|
|
fmt.Fprintf(w, "Status: %s\n", initStatus(result))
|
|
fmt.Fprintf(w, "Repository URL: %s\n", result.GitHTTPURL)
|
|
if result.ConfigWarning != "" {
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w, "Git credential saved, but Git helper was not configured")
|
|
fmt.Fprintf(w, "Reason: %s\n", result.ConfigWarning)
|
|
fmt.Fprintf(w, "Next step: lark-cli apps +git-credential-init --app-id %s\n", result.AppID)
|
|
return
|
|
}
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w, "Next step:")
|
|
fmt.Fprintf(w, " git clone %s\n", result.GitHTTPURL)
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var AppsGitCredentialRemove = common.Shortcut{
|
|
Service: appsService,
|
|
Command: "+git-credential-remove",
|
|
Description: "Remove local Git credentials and the URL-scoped Git helper for an app repository",
|
|
Risk: "write",
|
|
Tips: []string{
|
|
"Example: lark-cli apps +git-credential-remove --app-id <app_id>",
|
|
},
|
|
Scopes: []string{},
|
|
AuthTypes: []string{"user"},
|
|
HasFormat: true,
|
|
Flags: []common.Flag{
|
|
{Name: "app-id", Desc: "app ID", Required: true},
|
|
},
|
|
Validate: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
|
if strings.TrimSpace(rctx.Str("app-id")) == "" {
|
|
return appsValidationParamError("--app-id", "--app-id is required")
|
|
}
|
|
if err := validate.ResourceName(strings.TrimSpace(rctx.Str("app-id")), "--app-id"); err != nil {
|
|
return appsValidationParamError("--app-id", "%v", err).WithCause(err)
|
|
}
|
|
return nil
|
|
},
|
|
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
|
appID := strings.TrimSpace(rctx.Str("app-id"))
|
|
return common.NewDryRunAPI().
|
|
Desc("Preview local Git credential cleanup (no API call; would clean up local-only state).").
|
|
Set("mode", "local-cleanup-only").
|
|
Set("action", "remove_local_git_credential").
|
|
Set("app_id", appID).
|
|
Set("metadata_file", appKeyPath(appID, gitcred.MetadataFilename)).
|
|
Set("effects", []string{
|
|
"read app-scoped git credential metadata",
|
|
"remove the saved PAT from the local system credential store",
|
|
"remove the app-scoped Git helper from global git config when present",
|
|
"delete the local metadata record after cleanup succeeds",
|
|
})
|
|
},
|
|
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
|
appID := strings.TrimSpace(rctx.Str("app-id"))
|
|
manager := newGitCredentialManager(appID, rctx.Factory.Keychain, nil)
|
|
result, err := manager.Remove(ctx, profileFromConfig(rctx.Config), appID)
|
|
if err != nil {
|
|
return gitCredentialLocalError("Remove local app Git credential", err)
|
|
}
|
|
payload := map[string]interface{}{
|
|
"app_id": result.AppID,
|
|
"removed": result.Removed,
|
|
}
|
|
if result.ConfigWarning != "" {
|
|
payload["git_config_warning"] = result.ConfigWarning
|
|
}
|
|
rctx.OutFormat(payload, nil, func(w io.Writer) {
|
|
if !result.Removed {
|
|
fmt.Fprintln(w, "No local Git credential found")
|
|
return
|
|
}
|
|
fmt.Fprintln(w, "Git credential removed")
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintf(w, "App ID: %s\n", result.AppID)
|
|
if len(result.Records) > 0 {
|
|
fmt.Fprintf(w, "Repository URL: %s\n", result.Records[0].GitHTTPURL)
|
|
}
|
|
fmt.Fprintln(w, "Status: removed")
|
|
if result.ConfigWarning != "" {
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w, "Git config cleanup warning")
|
|
fmt.Fprintf(w, "Reason: %s\n", result.ConfigWarning)
|
|
}
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
var AppsGitCredentialList = common.Shortcut{
|
|
Service: appsService,
|
|
Command: "+git-credential-list",
|
|
Description: "List local Git credentials for app repositories",
|
|
Risk: "read",
|
|
Tips: []string{
|
|
"Example: lark-cli apps +git-credential-list",
|
|
},
|
|
Scopes: []string{},
|
|
AuthTypes: []string{"user"},
|
|
HasFormat: true,
|
|
DryRun: func(ctx context.Context, rctx *common.RuntimeContext) *common.DryRunAPI {
|
|
return common.NewDryRunAPI().
|
|
Desc("Preview local Git credential listing (no API call, read-only local state).").
|
|
Set("mode", "local-read-only").
|
|
Set("action", "list_local_git_credentials").
|
|
Set("storage_root", filepath.Join(core.GetConfigDir(), storageRoot)).
|
|
Set("reads", []string{
|
|
"scan app-scoped git credential metadata under the CLI config directory",
|
|
"derive per-app repository URLs and local credential status from local metadata",
|
|
})
|
|
},
|
|
Execute: func(ctx context.Context, rctx *common.RuntimeContext) error {
|
|
records, err := listGitCredentialRecords(rctx.Factory.Keychain, time.Now)
|
|
if err != nil {
|
|
return gitCredentialLocalError("List local app Git credentials", err)
|
|
}
|
|
payload := map[string]interface{}{
|
|
"count": len(records),
|
|
"credentials": gitCredentialListPayload(records),
|
|
}
|
|
rctx.OutFormat(payload, nil, func(w io.Writer) {
|
|
if len(records) == 0 {
|
|
fmt.Fprintln(w, "No Git credentials initialized")
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w, "Next step: lark-cli apps +git-credential-init --app-id <app_id>")
|
|
return
|
|
}
|
|
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
|
fmt.Fprintln(tw, "App ID\tRepository URL\tStatus")
|
|
for _, record := range records {
|
|
fmt.Fprintf(tw, "%s\t%s\t%s\n", record.AppID, record.GitHTTPURL, gitCredentialDisplayStatus(record.Status))
|
|
}
|
|
_ = tw.Flush()
|
|
fmt.Fprintln(w)
|
|
fmt.Fprintln(w, "Profile switches do not remove old URL-scoped Git helpers automatically.")
|
|
fmt.Fprintln(w, "Cleanup: lark-cli apps +git-credential-remove --app-id <app_id>")
|
|
})
|
|
return nil
|
|
},
|
|
}
|
|
|
|
// InstallOnApps attaches hidden, apps-domain commands that are not regular
|
|
// shortcuts. git-credential-helper must speak Git's stdin/stdout protocol
|
|
// directly, so it intentionally does not use the shortcut JSON envelope.
|
|
func InstallOnApps(parent *cobra.Command, f *cmdutil.Factory) {
|
|
parent.AddCommand(newGitCredentialHelperCommand(f))
|
|
}
|
|
|
|
func newGitCredentialHelperCommand(f *cmdutil.Factory) *cobra.Command {
|
|
cmd := &cobra.Command{
|
|
Use: "git-credential-helper get|store|erase",
|
|
Short: "Git credential helper for app repositories",
|
|
Hidden: true,
|
|
Args: cobra.ExactArgs(1),
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
appID, _ := cmd.Flags().GetString("app-id")
|
|
return runGitCredentialHelper(cmd.Context(), f, strings.TrimSpace(appID), args[0])
|
|
},
|
|
}
|
|
cmd.Flags().String("app-id", "", "app ID")
|
|
_ = cmd.Flags().MarkHidden("app-id")
|
|
return cmd
|
|
}
|
|
|
|
type runtimeIssuer struct {
|
|
rctx *common.RuntimeContext
|
|
}
|
|
|
|
func (i runtimeIssuer) Issue(ctx context.Context, appID string, profile gitcred.ProfileContext) (*gitcred.IssuedCredential, error) {
|
|
resp, err := i.rctx.DoAPI(&larkcore.ApiReq{
|
|
HttpMethod: http.MethodGet,
|
|
ApiPath: issuePath(appID),
|
|
})
|
|
data, err := parseIssueCredentialData(resp, err, i.rctx.APIClassifyContext())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return issuedFromData(appID, data)
|
|
}
|
|
|
|
type factoryIssuer struct {
|
|
f *cmdutil.Factory
|
|
}
|
|
|
|
func (i factoryIssuer) Issue(ctx context.Context, appID string, profile gitcred.ProfileContext) (*gitcred.IssuedCredential, error) {
|
|
cfg, err := i.f.Config()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if cfg.UserOpenId == "" {
|
|
return nil, errs.NewAuthenticationError(errs.SubtypeTokenMissing, "not logged in").
|
|
WithHint("run `lark-cli auth login --scope \"spark:app:read\"`")
|
|
}
|
|
ac, err := i.f.NewAPIClientWithConfig(cfg)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req := &larkcore.ApiReq{
|
|
HttpMethod: http.MethodGet,
|
|
ApiPath: issuePath(appID),
|
|
}
|
|
ctx = contextWithGitCredentialHelperShortcut(ctx)
|
|
var opts []larkcore.RequestOptionFunc
|
|
if optFn := cmdutil.ShortcutHeaderOpts(ctx); optFn != nil {
|
|
opts = append(opts, optFn)
|
|
}
|
|
resp, err := ac.DoSDKRequest(ctx, req, core.AsUser, opts...)
|
|
data, err := parseIssueCredentialData(resp, err, errclass.ClassifyContext{
|
|
Brand: string(cfg.Brand),
|
|
AppID: cfg.AppID,
|
|
Identity: string(core.AsUser),
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return issuedFromData(appID, data)
|
|
}
|
|
|
|
func contextWithGitCredentialHelperShortcut(ctx context.Context) context.Context {
|
|
if _, ok := cmdutil.ShortcutNameFromContext(ctx); ok {
|
|
return ctx
|
|
}
|
|
return cmdutil.ContextWithShortcut(ctx, gitCredentialHelperReportedShortcut, uuid.New().String())
|
|
}
|
|
|
|
func runGitCredentialHelper(ctx context.Context, f *cmdutil.Factory, appID, action string) error {
|
|
if f == nil || f.IOStreams == nil {
|
|
return nil
|
|
}
|
|
if appID == "" {
|
|
fmt.Fprintln(f.IOStreams.ErrOut, "Git credential unavailable: missing app_id; rerun lark-cli apps +git-credential-init --app-id <app_id>")
|
|
return nil
|
|
}
|
|
manager := newGitCredentialManager(appID, f.Keychain, factoryIssuer{f: f})
|
|
switch action {
|
|
case "get":
|
|
input, err := gitcred.ParseCredentialInput(f.IOStreams.In)
|
|
if err != nil {
|
|
fmt.Fprintf(f.IOStreams.ErrOut, "Git credential unavailable: %s\n", err)
|
|
return nil
|
|
}
|
|
cfg, err := f.Config()
|
|
if err != nil {
|
|
fmt.Fprintf(f.IOStreams.ErrOut, "Git credential unavailable: %s\n", err)
|
|
return nil
|
|
}
|
|
return manager.Get(ctx, input, profileFromConfig(cfg), f.IOStreams.Out, f.IOStreams.ErrOut)
|
|
case "store":
|
|
return manager.StoreCredential(f.IOStreams.In)
|
|
case "erase":
|
|
return manager.Erase(f.IOStreams.In)
|
|
default:
|
|
fmt.Fprintf(f.IOStreams.ErrOut, "unsupported git credential action %q\n", action)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func newGitCredentialManager(appID string, kc keychain.KeychainAccess, issuer gitcred.Issuer) *gitcred.Manager {
|
|
storage := gitCredentialAppStorage{}
|
|
return gitcred.NewManager(gitcred.NewAppStore(appID, storage), gitcred.NewSecretStore(kc), gitcred.GlobalGitConfig{}, issuer)
|
|
}
|
|
|
|
func listGitCredentialRecords(kc keychain.KeychainAccess, now func() time.Time) ([]gitcred.ListRecord, error) {
|
|
storage := gitCredentialAppStorage{}
|
|
appIDs, err := storage.ListAppIDs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records := make([]gitcred.ListRecord, 0, len(appIDs))
|
|
for _, appID := range appIDs {
|
|
manager := newGitCredentialManager(appID, kc, nil)
|
|
manager.Now = now
|
|
result, err := manager.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
records = append(records, result.Records...)
|
|
}
|
|
sort.Slice(records, func(i, j int) bool {
|
|
if records[i].AppID == records[j].AppID {
|
|
return records[i].GitHTTPURL < records[j].GitHTTPURL
|
|
}
|
|
return records[i].AppID < records[j].AppID
|
|
})
|
|
return records, nil
|
|
}
|
|
|
|
func gitCredentialListPayload(records []gitcred.ListRecord) []map[string]interface{} {
|
|
out := make([]map[string]interface{}, 0, len(records))
|
|
for _, record := range records {
|
|
out = append(out, map[string]interface{}{
|
|
"app_id": record.AppID,
|
|
"repository_url": record.GitHTTPURL,
|
|
"status": gitCredentialDisplayStatus(record.Status),
|
|
})
|
|
}
|
|
return out
|
|
}
|
|
|
|
func gitCredentialDisplayStatus(status string) string {
|
|
if status == gitcred.ListStatusExpired {
|
|
return "refresh_required"
|
|
}
|
|
return status
|
|
}
|
|
|
|
func profileFromConfig(cfg *core.CliConfig) gitcred.ProfileContext {
|
|
if cfg == nil {
|
|
return gitcred.ProfileContext{}
|
|
}
|
|
return gitcred.ProfileContext{
|
|
Profile: cfg.ProfileName,
|
|
ProfileAppID: cfg.AppID,
|
|
UserOpenID: cfg.UserOpenId,
|
|
}
|
|
}
|
|
|
|
func issuePath(appID string) string {
|
|
return strings.Replace(gitCredentialIssuePath, ":app_id", url.PathEscape(strings.TrimSpace(appID)), 1)
|
|
}
|
|
|
|
func gitCredentialIssueParams(appID string) map[string]interface{} {
|
|
return map[string]interface{}{"app_id": strings.TrimSpace(appID)}
|
|
}
|
|
|
|
func initStatus(result *gitcred.InitResult) string {
|
|
if result != nil && result.Refreshed {
|
|
return "refreshed"
|
|
}
|
|
return "initialized"
|
|
}
|
|
|
|
func gitCredentialLocalError(action string, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
// Typed errors pass through unchanged; everything the apps domain and the
|
|
// shared runtime produce is typed, so there is no legacy envelope to forward.
|
|
if _, ok := errs.UnwrapTypedError(err); ok {
|
|
return err
|
|
}
|
|
return &errs.ConfigError{Problem: errs.Problem{
|
|
Category: errs.CategoryConfig,
|
|
Subtype: errs.SubtypeInvalidConfig,
|
|
Message: fmt.Sprintf("%s: %s", action, err),
|
|
Hint: "retry the command; if the local Git credential state is damaged, rerun `lark-cli apps +git-credential-init --app-id <app_id>` or remove the app credential again",
|
|
}, Cause: err}
|
|
}
|
|
|
|
func issuedFromData(appID string, data map[string]interface{}) (*gitcred.IssuedCredential, error) {
|
|
source := data
|
|
for _, key := range []string{"credential", "git_credential", "gitInfo", "git_info"} {
|
|
if nested, ok := data[key].(map[string]interface{}); ok {
|
|
source = nested
|
|
break
|
|
}
|
|
}
|
|
issued := &gitcred.IssuedCredential{
|
|
AppID: firstString(source, "app_id", appID),
|
|
GitHTTPURL: firstString(source, "gitURL", "GitURL", "GitUrl", "gitUrl", "git_url", "git_http_url", "repository_url"),
|
|
Username: firstString(source, "username"),
|
|
PAT: firstString(source, "token", "Token", "pat", "password"),
|
|
ExpiresAt: firstInt64(source, "expiredTime", "ExpiredTime", "expired_time", "expires_at"),
|
|
}
|
|
if issued.AppID == "" {
|
|
issued.AppID = appID
|
|
}
|
|
if issued.GitHTTPURL == "" {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "Issue app Git credential: response missing gitURL")
|
|
}
|
|
if issued.PAT == "" {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse, "Issue app Git credential: response missing token")
|
|
}
|
|
return issued, nil
|
|
}
|
|
|
|
// parseIssueCredentialData turns the git-credential issue response into the
|
|
// credential data map. A standard Lark envelope (top-level "code") and any
|
|
// HTTP error status route through the shared response classifier, so generic
|
|
// codes (missing scope, app not authorized) and 5xx statuses keep their
|
|
// canonical category/subtype/retryable classification. The endpoint's
|
|
// non-standard success shapes — direct git info or a BaseResp wrapper — are
|
|
// handled locally.
|
|
func parseIssueCredentialData(resp *larkcore.ApiResp, err error, cc errclass.ClassifyContext) (map[string]any, error) {
|
|
if err != nil {
|
|
return nil, redactGitCredentialIssueError(client.WrapDoAPIError(err))
|
|
}
|
|
detail := logIDDetail(resp)
|
|
if resp == nil || len(resp.RawBody) == 0 {
|
|
return nil, errs.NewInternalError(errs.SubtypeInvalidResponse,
|
|
"Issue app Git credential: empty response body")
|
|
}
|
|
var result map[string]any
|
|
jsonErr := json.Unmarshal(resp.RawBody, &result)
|
|
_, hasCode := result["code"]
|
|
if jsonErr != nil || hasCode || resp.StatusCode >= http.StatusBadRequest {
|
|
data, cerr := common.ClassifyAPIResponseWith(resp, cc)
|
|
if cerr != nil {
|
|
return nil, redactGitCredentialIssueError(withAppsHint(cerr, gitCredentialIssueHint))
|
|
}
|
|
if data != nil {
|
|
result = data
|
|
}
|
|
// data == nil: a code==0 envelope whose fields sit beside "code" instead
|
|
// of under "data" — keep the locally-unmarshalled top-level object.
|
|
} else if err := checkGitInfoBaseResp(result, logIDString(resp)); err != nil {
|
|
return nil, err
|
|
}
|
|
if detail != nil {
|
|
if result == nil {
|
|
result = map[string]any{}
|
|
}
|
|
for k, v := range detail {
|
|
result[k] = v
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func checkGitInfoBaseResp(result map[string]any, logID string) error {
|
|
for _, key := range []string{"BaseResp", "baseResp", "base_resp"} {
|
|
baseResp, ok := result[key].(map[string]any)
|
|
if !ok {
|
|
continue
|
|
}
|
|
code := firstInt64(baseResp, "StatusCode", "statusCode", "status_code")
|
|
if code == 0 {
|
|
return nil
|
|
}
|
|
message := firstString(baseResp, "StatusMessage", "statusMessage", "status_message")
|
|
if message == "" {
|
|
message = "Git credential API returned non-zero BaseResp status"
|
|
}
|
|
message = gitcred.RedactCredentialText(message)
|
|
baseErr := errs.NewAPIError(errs.SubtypeUnknown, "Issue app Git credential: %s", message).WithCode(int(code))
|
|
if logID != "" {
|
|
baseErr = baseErr.WithLogID(logID)
|
|
}
|
|
return baseErr
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func redactGitCredentialIssueError(err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
if p, ok := errs.ProblemOf(err); ok {
|
|
p.Message = gitcred.RedactCredentialText(p.Message)
|
|
p.Hint = gitcred.RedactCredentialText(p.Hint)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func logIDDetail(resp *larkcore.ApiResp) map[string]any {
|
|
logID := logIDString(resp)
|
|
if logID == "" {
|
|
return nil
|
|
}
|
|
return map[string]any{"log_id": logID}
|
|
}
|
|
|
|
func logIDString(resp *larkcore.ApiResp) string {
|
|
if resp == nil {
|
|
return ""
|
|
}
|
|
return resp.Header.Get("x-tt-logid")
|
|
}
|
|
|
|
func firstString(data map[string]interface{}, keys ...string) string {
|
|
for _, key := range keys {
|
|
if v, ok := data[key].(string); ok && strings.TrimSpace(v) != "" {
|
|
return strings.TrimSpace(v)
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func firstInt64(data map[string]interface{}, keys ...string) int64 {
|
|
for _, key := range keys {
|
|
switch v := data[key].(type) {
|
|
case int64:
|
|
return v
|
|
case int:
|
|
return int64(v)
|
|
case float64:
|
|
return int64(v)
|
|
case json.Number:
|
|
n, _ := v.Int64()
|
|
return n
|
|
case string:
|
|
n, _ := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
return n
|
|
}
|
|
}
|
|
return 0
|
|
}
|