feat(auth): add edition overlay auth credentials and token marker for embedded mode

- Add AuthClientID and AuthClientFromMCP fields to edition Hooks, allowing overlays to override the default OAuth client ID and route auth through MCP endpoints.

- Introduce token marker file (token.json) mechanism so the host application in embedded mode can detect authentication state without accessing the keychain.

- Update SaveTokenData/DeleteTokenData to write/remove the marker file when running in embedded mode.

- Update ClientID() and IsClientIDFromMCP() to respect edition overrides.
This commit is contained in:
wxianfeng committed 2026-04-10 17:02:18 +08:00
1 parent 953b422295
commit aec852f971
3 files changed
+56 -8

No files matched your search

+6 -1
View File
@@ -18,6 +18,8 @@ import (
"path/filepath"
"strings"
"sync"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
const (
@@ -110,7 +112,7 @@ func SetClientIDFromMCP(id string) {
func IsClientIDFromMCP() bool {
clientMu.RLock()
defer clientMu.RUnlock()
return clientIDFromMCP
return clientIDFromMCP || edition.Get().AuthClientFromMCP
}
// GetUserAccessTokenURL returns the appropriate token exchange URL.
@@ -189,6 +191,9 @@ func ClientID() string {
if override != "" {
return override
}
if id := edition.Get().AuthClientID; id != "" {
return id
}
// Try loading from persisted app config
if id, _ := ResolveAppCredentials(getDefaultConfigDir()); id != "" {
return id
+46 -7
View File
@@ -20,7 +20,11 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"path/filepath"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
// TokenData holds the OAuth token set persisted to disk.
@@ -61,10 +65,46 @@ func (t *TokenData) HasPersistentCode() bool {
return t != nil && t.PersistentCode != ""
}
const tokenJSONFile = "token.json"
// TokenMarker is a lightweight file the host application reads to detect
// whether the CLI has a valid token without accessing the keychain.
type TokenMarker struct {
UpdatedAt string `json:"updated_at"`
}
// WriteTokenMarker writes a token.json marker containing only an updated_at
// timestamp. The host application uses this file's presence and mtime to
// decide whether it needs to trigger a new auth exchange.
func WriteTokenMarker(configDir string) error {
marker := TokenMarker{UpdatedAt: time.Now().Format(time.RFC3339)}
data, _ := json.MarshalIndent(marker, "", " ")
if err := os.MkdirAll(configDir, 0o700); err != nil {
return err
}
tmp := filepath.Join(configDir, tokenJSONFile+".tmp")
if err := os.WriteFile(tmp, data, 0o600); err != nil {
return err
}
return os.Rename(tmp, filepath.Join(configDir, tokenJSONFile))
}
// DeleteTokenMarker removes the token.json marker file.
func DeleteTokenMarker(configDir string) error {
return os.Remove(filepath.Join(configDir, tokenJSONFile))
}
// SaveTokenData saves TokenData to the platform keychain.
// Uses the new keychain-based storage with random master key for better security.
// In embedded mode, it also writes a token.json marker file so the host
// application can detect authentication state without accessing the keychain.
func SaveTokenData(configDir string, data *TokenData) error {
return SaveTokenDataKeychain(data)
if err := SaveTokenDataKeychain(data); err != nil {
return err
}
if edition.Get().IsEmbedded {
_ = WriteTokenMarker(configDir)
}
return nil
}
// LoadTokenData reads TokenData from the platform keychain.
@@ -91,14 +131,13 @@ func LoadTokenData(configDir string) (*TokenData, error) {
}
// DeleteTokenData removes token data from both keychain and legacy storage.
// In embedded mode, it also removes the token.json marker file.
func DeleteTokenData(configDir string) error {
// Delete from keychain
keychainErr := DeleteTokenDataKeychain()
// Also clean up any legacy .data file
legacyErr := DeleteSecureData(configDir)
// Return keychain error if any, otherwise legacy error
if edition.Get().IsEmbedded {
_ = DeleteTokenMarker(configDir)
}
if keychainErr != nil {
return keychainErr
}
+4
View File
@@ -77,6 +77,10 @@ type Hooks struct {
OnAuthError func(configDir string, err error) error
TokenProvider func(ctx context.Context, fallback func() (string, error)) (string, error)
// --- auth credentials (overlay-only) ---
AuthClientID string // non-empty overrides DefaultClientID
AuthClientFromMCP bool // true routes OAuth through MCP endpoints
// --- product & endpoint ---
StaticServers func() []ServerInfo // non-nil → skip Market discovery
VisibleProducts func() []string // non-nil → override help visibility