Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/cmdutil/factory_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ func buildSDKTransport() http.RoundTripper {
var sdkTransport http.RoundTripper = util.SharedTransport()
sdkTransport = &RetryTransport{Base: sdkTransport}
sdkTransport = &UserAgentTransport{Base: sdkTransport}
sdkTransport = &BuildHeaderTransport{Base: sdkTransport}
sdkTransport = &auth.SecurityPolicyTransport{Base: sdkTransport}
return wrapWithExtension(sdkTransport)
}
Expand Down
116 changes: 115 additions & 1 deletion internal/cmdutil/secheader.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,20 +6,36 @@
import (
"context"
"net/http"

"reflect"
"runtime/debug"
"strings"
"sync"

"github.com/larksuite/cli/extension/credential"
"github.com/larksuite/cli/extension/fileio"
exttransport "github.com/larksuite/cli/extension/transport"
"github.com/larksuite/cli/internal/build"
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
)

const (
HeaderSource = "X-Cli-Source"
HeaderVersion = "X-Cli-Version"
HeaderBuild = "X-Cli-Build"
HeaderShortcut = "X-Cli-Shortcut"
HeaderExecutionId = "X-Cli-Execution-Id"

SourceValue = "lark-cli"

HeaderUserAgent = "User-Agent"

// BuildKindOfficial / BuildKindExtended / BuildKindUnknown are the values
// reported in the X-Cli-Build header; see DetectBuildKind for semantics.
BuildKindOfficial = "official"
BuildKindExtended = "extended"
BuildKindUnknown = "unknown"

officialModulePath = "github.com/larksuite/cli"
)

// UserAgentValue returns the User-Agent value: "lark-cli/{version}".
Expand All @@ -32,10 +48,108 @@
h := make(http.Header)
h.Set(HeaderSource, SourceValue)
h.Set(HeaderVersion, build.Version)
h.Set(HeaderBuild, DetectBuildKind())
h.Set(HeaderUserAgent, UserAgentValue())
return h
}

var (
buildKindOnce sync.Once
buildKindVal string
)

// DetectBuildKind reports whether this binary is the official CLI, an
// extended/repackaged build, or unknown. The result is cached via sync.Once
// so it is computed only on the first call.
//
// IMPORTANT: must NOT be called from any package init(). Go's init ordering
// follows the import graph; ISV providers registered via blank import may not
// have run yet, which would misclassify an extended build as official. Call
// only when handling an actual request (e.g. from BaseSecurityHeaders).
func DetectBuildKind() string {
buildKindOnce.Do(func() {
buildKindVal = computeBuildKind()
})
return buildKindVal
}

// computeBuildKind performs the actual detection without any caching.
// Exposed for tests. Gathers runtime/global inputs and delegates the pure
// branching logic to classifyBuild so that logic can be unit-tested without
// mutating process-wide provider registries.
func computeBuildKind() string {
info, ok := debug.ReadBuildInfo()
mainPath := ""
if ok {
mainPath = info.Main.Path
}

credProviders := credential.Providers()
creds := make([]any, len(credProviders))
for i, p := range credProviders {
creds[i] = p
}

var tp any
if p := exttransport.GetProvider(); p != nil {
tp = p
}

Check warning on line 96 in internal/cmdutil/secheader.go

View check run for this annotation

Codecov / codecov/patch

internal/cmdutil/secheader.go#L95-L96

Added lines #L95 - L96 were not covered by tests
var fp any
if p := fileio.GetProvider(); p != nil {
fp = p
}
return classifyBuild(mainPath, ok, creds, tp, fp)
}

// classifyBuild is the pure classification logic used by computeBuildKind.
// Callers supply concrete values so every branch is reachable from tests
// without touching debug.ReadBuildInfo or the extension registries.
//
// Priority order mirrors the design doc:
// 1. no build info → unknown
// 2. main module path not the official one → extended (ISV wrapper)
// 3. any non-builtin provider (credential / transport / fileio) → extended
// 4. otherwise → official
func classifyBuild(mainPath string, haveBuildInfo bool, credProviders []any, transportProvider, fileioProvider any) string {
if !haveBuildInfo {
return BuildKindUnknown
}
if mainPath != "" && mainPath != officialModulePath {
return BuildKindExtended
}
for _, p := range credProviders {
if !isBuiltinProvider(p) {
return BuildKindExtended
}
}
if transportProvider != nil && !isBuiltinProvider(transportProvider) {
return BuildKindExtended
}
if fileioProvider != nil && !isBuiltinProvider(fileioProvider) {
return BuildKindExtended
}
return BuildKindOfficial
}

// isBuiltinProvider reports whether p is declared under the official module
// path. Third-party providers live under their own module and fail this check.
// Using reflect.PkgPath makes this robust against Name() spoofing since
// package paths are fixed at compile time.
func isBuiltinProvider(p any) bool {
if p == nil {
return false
}
t := reflect.TypeOf(p)
if t == nil {
return false
}

Check warning on line 145 in internal/cmdutil/secheader.go

View check run for this annotation

Codecov / codecov/patch

internal/cmdutil/secheader.go#L144-L145

Added lines #L144 - L145 were not covered by tests
if t.Kind() == reflect.Ptr {
t = t.Elem()
}
pkg := t.PkgPath()
return pkg == officialModulePath || strings.HasPrefix(pkg, officialModulePath+"/")
}

// ── Context utilities ──

type ctxKey string
Expand Down
34 changes: 34 additions & 0 deletions internal/cmdutil/secheader_sidecar_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
// SPDX-License-Identifier: MIT

//go:build authsidecar

package cmdutil

import (
"testing"

sidecarcred "github.com/larksuite/cli/extension/credential/sidecar"
sidecartrans "github.com/larksuite/cli/extension/transport/sidecar"
)

// TestIsBuiltinProvider_SidecarProviders locks the classification for the
// sidecar-mode providers enumerated in design doc §3.3.2 as "官方自带". These
// types only compile when the `authsidecar` build tag is active, so the test
// is guarded by the same tag.
func TestIsBuiltinProvider_SidecarProviders(t *testing.T) {
cases := []struct {
name string
provider any
}{
{"sidecar credential provider", &sidecarcred.Provider{}},
{"sidecar transport provider", &sidecartrans.Provider{}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if !isBuiltinProvider(tc.provider) {
t.Fatalf("%T must be classified as builtin (PkgPath under %s)", tc.provider, officialModulePath)
}
})
}
}
Loading
Loading