// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package config provides shared constants used across multiple internal // packages. Only cross-cutting values belong here; package-private // constants should remain in their own package. package config import ( "net/url" "os" "path/filepath" "strings" "time" ) // ── File permissions ──────────────────────────────────────────────────── // These are used consistently by auth, security, and cache packages to // protect sensitive data on disk. const ( // DirPerm is the permission mode for directories that hold sensitive // data (token store, cache, lock files). Owner-only rwx. DirPerm os.FileMode = 0o700 // FilePerm is the permission mode for sensitive files (encrypted // tokens, cache entries, lock files). Owner-only rw. FilePerm os.FileMode = 0o600 ) // ── HTTP timeouts ─────────────────────────────────────────────────────── // Shared across transport, market, auth, and device-flow packages. const ( // HTTPTimeout is the default timeout for outgoing HTTP requests // (market registry, MCP JSON-RPC, device flow). HTTPTimeout = 30 * time.Second // OAuthTimeout is the timeout for OAuth token exchange/refresh // requests, which are latency-sensitive and should fail fast. OAuthTimeout = 15 * time.Second // DiscoveryTimeout bounds the time spent on live registry + runtime // discovery before falling back to cache. DiscoveryTimeout = 10 * time.Second // LockTimeout is how long to wait when acquiring a cross-process // file lock for token operations. LockTimeout = 10 * time.Second ) // ── Response limits ───────────────────────────────────────────────────── const ( // MaxResponseBodySize limits the amount of data read from a single // HTTP response to prevent memory exhaustion from malicious servers. MaxResponseBodySize = 10 * 1024 * 1024 // 10 MB ) // ── Cache ─────────────────────────────────────────────────────────────── const ( // DefaultPartition is the cache partition used when no tenant/org // context is available. DefaultPartition = "default/default" ) // IsOpenEdition reports whether an edition name maps to the open-source core. // // This helper takes the edition name as a parameter instead of calling // edition.Get() so that pkg/config remains a leaf dependency — importable // from internal/cli, internal/app, internal/cache, etc. without risking // import cycles. func IsOpenEdition(name string) bool { name = strings.TrimSpace(name) return name == "" || name == "open" } // EditionPartition returns the cache partition for a given edition name. // The open-source core (name == "" or "open") uses DefaultPartition; every // other edition gets its own namespace to prevent cross-edition data // leakage in the disk cache. func EditionPartition(name string) string { name = strings.TrimSpace(name) if IsOpenEdition(name) { return DefaultPartition } return name + "/default" } // EditionFileName returns the edition-partitioned file name for base+ext. func EditionFileName(name, base, ext string) string { name = strings.TrimSpace(name) if IsOpenEdition(name) { return base + ext } return base + "-" + name + ext } // ── Auth flow timeouts ────────────────────────────────────────────────── const ( // ManualTokenExpiry is the default lifetime for manually imported tokens. ManualTokenExpiry = 24 * time.Hour // DeviceFlowTimeout is the maximum wait time for device-flow authorization. DeviceFlowTimeout = 16 * time.Minute // OAuthFlowTimeout is the maximum wait time for browser-based OAuth. OAuthFlowTimeout = 6 * time.Minute // DefaultAccessTokenExpiry is the default access token lifetime in seconds // when the server does not return an explicit expires_in value. DefaultAccessTokenExpiry = 7200 // DefaultRefreshTokenLifetime is the default refresh token lifetime. DefaultRefreshTokenLifetime = 30 * 24 * time.Hour ) // ── Market ────────────────────────────────────────────────────────────── const ( // DefaultFetchServersLimit is the maximum number of servers to fetch // from the market registry in a single request. DefaultFetchServersLimit = 200 ) // ── Upload limits ─────────────────────────────────────────────────────── const ( // MaxUploadFileSize is the maximum file size for attachment uploads. MaxUploadFileSize int64 = 100 * 1024 * 1024 // 100 MB ) // ── Plugin system ────────────────────────────────────────────────────── const ( // PluginUserDir is the subdirectory under ~/.dws/plugins/ where all // third-party plugins are installed. Every plugin — whether authored // by the DingTalk team or anyone else — lives here with equal status. PluginUserDir = "user" // PluginDataDir is the subdirectory under ~/.dws/plugins/ for // plugin persistent data that survives across version updates. PluginDataDir = "data" // PluginHookTimeout is the default timeout for plugin hook commands. PluginHookTimeout = 30 * time.Second ) // ── Platform URLs ──────────────────────────────────────────────────────────── // Shared across auth, errors, and device-flow packages. const ( // DefaultMCPBaseURL is the DingTalk MCP base URL. // Override at runtime via ~/.dws/mcp_url file. DefaultMCPBaseURL = "https://mcp.dingtalk.com" // ManagedMCPURLRegionFileName records an MCP URL written automatically by // login region selection, so a later region change does not delete a user's // explicit MCP override. ManagedMCPURLRegionFileName = "mcp_url.login_region" // DefaultTerminalBaseURL is the DingTalk developer platform base URL. // Override at runtime via ~/.dws/terminal_url file. DefaultTerminalBaseURL = "https://open-dev.dingtalk.com" // InternationalTerminalBaseURL is the DingTalk international developer // platform base URL. InternationalTerminalBaseURL = "https://open-dev.dingtalk.io" // DeveloperSettingsPath is the path to the organization developer // settings page (CLI access management). DeveloperSettingsPath = "/fe/old#/developerSettings" ) // DefaultConfigDir returns the default DWS configuration directory. // Priority: DWS_CONFIG_DIR env var > ~/.dws func DefaultConfigDir() string { if envDir := os.Getenv("DWS_CONFIG_DIR"); envDir != "" { return envDir } homeDir, err := os.UserHomeDir() if err != nil { return ".dws" } return filepath.Join(homeDir, ".dws") } // GetMCPBaseURL returns the MCP base URL with priority: // 1. ~/.dws/mcp_url file content (for custom environment) // 2. Default value (https://mcp.dingtalk.com) func GetMCPBaseURL() string { mcpURLPath := filepath.Join(DefaultConfigDir(), "mcp_url") if data, err := os.ReadFile(mcpURLPath); err == nil { if u := strings.TrimSpace(string(data)); u != "" { return u } } return DefaultMCPBaseURL } // GetTerminalBaseURL returns the terminal base URL with priority: // 1. ~/.dws/terminal_url file content (for custom environments) // 2. International terminal when the configured MCP endpoint uses .io // 3. Default value (https://open-dev.dingtalk.com) func GetTerminalBaseURL() string { terminalURLPath := filepath.Join(DefaultConfigDir(), "terminal_url") if data, err := os.ReadFile(terminalURLPath); err == nil { if u := strings.TrimSpace(string(data)); u != "" { return u } } if parsed, err := url.Parse(GetMCPBaseURL()); err == nil { host := strings.ToLower(parsed.Hostname()) if host == "dingtalk.io" || strings.HasSuffix(host, ".dingtalk.io") { return InternationalTerminalBaseURL } } return DefaultTerminalBaseURL } // GetDeveloperSettingsURL returns the full URL to the organization developer // settings page, derived from the terminal base URL. func GetDeveloperSettingsURL() string { return GetTerminalBaseURL() + DeveloperSettingsPath }