UNPKG

@lobehub/market-sdk

Version:
1 lines 115 kB
{"version":3,"sources":["../src/admin/MarketAdmin.ts","../src/core/BaseSDK.ts","../src/admin/services/AnalysisService.ts","../src/admin/services/PluginService.ts","../src/admin/services/SystemDependencyService.ts","../src/admin/services/SettingsService.ts","../src/admin/services/ReviewService.ts","../src/admin/services/PluginEnvService.ts","../src/market/market-sdk.ts","../src/market/services/DiscoveryService.ts","../src/market/services/PluginsService.ts","../src/types/admin.ts","../src/index.ts"],"sourcesContent":["import debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\nimport { MarketSDKOptions } from '@/types';\n\nimport {\n AnalysisService,\n PluginEnvService,\n PluginService,\n ReviewService,\n SettingsService,\n SystemDependencyService,\n} from './services';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin');\n\n/**\n * LobeHub Market Admin SDK Client\n *\n * Client for accessing administrative functionality of the LobeHub Marketplace.\n * This SDK provides privileged operations for managing plugins, reviews,\n * system settings, and dependencies. It requires admin-level authentication.\n */\nexport class MarketAdmin extends BaseSDK {\n /**\n * Market analysis service\n * Provides methods for accessing market analytics and statistics\n */\n readonly analysis: AnalysisService;\n\n /**\n * Plugin management service\n * Provides methods for creating, updating, and managing plugins\n */\n readonly plugins: PluginService;\n\n readonly env: PluginEnvService;\n\n /**\n * Review management service\n * Provides methods for moderating and managing user reviews\n */\n readonly reviews: ReviewService;\n\n /**\n * System settings management service\n * Provides methods for configuring marketplace settings\n */\n readonly settings: SettingsService;\n\n /**\n * System dependency management service\n * Provides methods for managing system dependencies required by plugins\n */\n readonly dependencies: SystemDependencyService;\n\n /**\n * Creates a new MarketAdmin instance\n *\n * @param options - Configuration options for the SDK\n */\n constructor(options: MarketSDKOptions = {}) {\n // Use admin-specific API key if available\n const apiKey = options.apiKey || process.env.MARKET_ADMIN_API_KEY;\n\n // Create shared token state object for all services\n const sharedTokenState = {\n accessToken: undefined,\n tokenExpiry: undefined,\n };\n\n super({ ...options, apiKey }, undefined, sharedTokenState);\n log('MarketAdmin instance created');\n\n // Initialize admin services with shared headers and token state for efficient reuse\n this.analysis = new AnalysisService(options, this.headers, sharedTokenState);\n this.plugins = new PluginService(options, this.headers, sharedTokenState);\n this.reviews = new ReviewService(options, this.headers, sharedTokenState);\n this.settings = new SettingsService(options, this.headers, sharedTokenState);\n this.dependencies = new SystemDependencyService(options, this.headers, sharedTokenState);\n this.env = new PluginEnvService(options, this.headers, sharedTokenState);\n }\n}\n","import debug from 'debug';\nimport { SignJWT } from 'jose';\nimport urlJoin from 'url-join';\n\nimport type { MarketSDKOptions, SharedTokenState } from '../types';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:core');\n\n/**\n * Base SDK class\n *\n * Provides shared request handling and authentication functionality that is used\n * by both the Market SDK and Admin SDK. This class handles the common concerns:\n * - API endpoint configuration\n * - Authentication header management\n * - HTTP request handling\n * - Error handling\n * - Query string building\n */\nexport class BaseSDK {\n /** Base API URL */\n protected apiBaseUrl: string;\n /** Base URL */\n protected baseUrl: string;\n /** OAuth URL */\n protected oauthBaseUrl: string;\n\n /** Default locale for requests that require localization */\n protected defaultLocale: string;\n\n /** HTTP headers to include with all requests */\n protected headers: Record<string, string>;\n\n private clientId?: string;\n private clientSecret?: string;\n private readonly initialAccessToken?: string;\n private accessToken?: string;\n private tokenExpiry?: number;\n\n // Shared token state for all instances\n private sharedTokenState?: SharedTokenState;\n\n /**\n * Creates a new BaseSDK instance\n *\n * @param options - Configuration options for the SDK\n * @param sharedHeaders - Optional shared headers object for reuse across services\n * @param sharedTokenState - Optional shared token state object for reuse across services\n */\n constructor(\n options: MarketSDKOptions = {},\n sharedHeaders?: Record<string, string>,\n sharedTokenState?: SharedTokenState,\n ) {\n // Set base URL from options, environment variable, or default to production URL\n this.baseUrl = options.baseURL || process.env.MARKET_BASE_URL || 'https://market.lobehub.com';\n this.apiBaseUrl = urlJoin(this.baseUrl, 'api');\n this.oauthBaseUrl = urlJoin(this.baseUrl, 'oauth');\n\n // Set default locale from options or use English as default\n this.defaultLocale = options.defaultLocale || 'en-US';\n\n this.initialAccessToken = options.accessToken;\n this.clientId = options.clientId;\n this.clientSecret = options.clientSecret;\n\n // Share token state across instances if provided\n this.sharedTokenState = sharedTokenState;\n\n // Get API key from options or environment variable\n const apiKey = options.apiKey || process.env.MARKET_API_KEY;\n\n // Either use shared headers or create new headers object\n if (sharedHeaders) {\n this.headers = sharedHeaders;\n log('Using shared headers object');\n } else {\n this.headers = {\n 'Content-Type': 'application/json',\n };\n log('Created new headers object');\n }\n\n // If an apiKey is provided, use it for authorization.\n // This will be overridden by M2M auth if credentials are also provided.\n if (apiKey) {\n this.headers.Authorization = `Bearer ${apiKey}`;\n }\n\n // If an accessToken is provided on init, it takes precedence.\n if (this.initialAccessToken) {\n this.headers.Authorization = `Bearer ${this.initialAccessToken}`;\n }\n\n log('BaseSDK instance created: %O', {\n baseUrl: this.baseUrl,\n defaultLocale: this.defaultLocale,\n hasApiKey: !!apiKey,\n hasInitialAccessToken: !!this.initialAccessToken,\n hasM2MCredentials: !!this.clientId,\n hasSharedTokenState: !!this.sharedTokenState,\n });\n }\n\n /**\n * Sends an HTTP request to the API and handles the response\n *\n * @param url - Request URL path (will be appended to baseUrl)\n * @param options - Fetch API request options\n * @returns Promise resolving to the parsed JSON response\n * @throws Error if the request fails\n */\n // eslint-disable-next-line no-undef\n protected async request<T>(url: string, options: RequestInit = {}): Promise<T> {\n const requestUrl = urlJoin(this.apiBaseUrl, url);\n log('Sending request: %s', requestUrl);\n\n // If no access token was provided on init, and we have M2M creds, run the auth flow.\n if (!this.initialAccessToken && this.clientId && this.clientSecret) {\n await this.setM2MAuthToken();\n }\n\n const response = await fetch(requestUrl, {\n ...options,\n headers: {\n ...this.headers,\n ...options.headers,\n },\n });\n\n if (!response.ok) {\n const errorMsg = `Request failed: ${response.status} ${response.statusText}`;\n try {\n const errorBody = await response.json();\n\n console.error('Request error: %s', JSON.stringify(errorBody));\n\n throw new Error(`${errorMsg} - ${JSON.stringify(errorBody)}`);\n } catch {\n throw new Error(errorMsg);\n }\n }\n\n log('Request successful: %s', url);\n return response.json() as Promise<T>;\n }\n\n /**\n * Builds a URL query string from a parameters object\n *\n * @param params - Object containing query parameters\n * @returns Formatted query string (including leading ? if params exist)\n */\n protected buildQueryString(params: Record<string, any>): string {\n const query = Object.entries(params)\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n .filter(([_, value]) => value !== undefined && value !== null)\n .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`)\n .join('&');\n\n return query ? `?${query}` : '';\n }\n\n /**\n * Sets an authentication token for API requests\n *\n * @param token - API authentication token\n */\n setAuthToken(token: string): void {\n log('Setting authentication token');\n this.accessToken = undefined;\n this.tokenExpiry = undefined;\n\n // Clear shared token state if it exists\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = undefined;\n this.sharedTokenState.tokenExpiry = undefined;\n }\n\n this.headers.Authorization = `Bearer ${token}`;\n }\n\n /**\n * Clears the authentication token\n */\n clearAuthToken(): void {\n log('Clearing authentication token');\n this.accessToken = undefined;\n this.tokenExpiry = undefined;\n\n // Clear shared token state if it exists\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = undefined;\n this.sharedTokenState.tokenExpiry = undefined;\n }\n\n delete this.headers.Authorization;\n }\n\n /**\n * Fetches an M2M access token.\n * This method is designed for server-side use cases where you need to manage the token lifecycle\n * (e.g., storing it in a cookie).\n *\n * @returns A promise that resolves to an object containing the access token and its expiry time.\n */\n public async fetchM2MToken(): Promise<{ accessToken: string; expiresIn: number }> {\n if (!this.clientId || !this.clientSecret) {\n throw new Error('clientId and clientSecret are required to fetch an M2M token.');\n }\n\n log('Fetching M2M token for server-side use');\n\n const assertion = await this.createClientAssertion();\n const tokenData = await this.exchangeTokenForServer(assertion);\n\n log('M2M token fetched successfully');\n\n return {\n accessToken: tokenData.access_token,\n expiresIn: tokenData.expires_in || 3600,\n };\n }\n\n private async createClientAssertion(): Promise<string> {\n if (!this.clientId || !this.clientSecret) {\n throw new Error('Missing clientId or clientSecret for M2M authentication.');\n }\n\n const secret = new TextEncoder().encode(this.clientSecret);\n\n const tokenEndpoint = `${this.oauthBaseUrl}/token`;\n\n return await new SignJWT({})\n .setProtectedHeader({ alg: 'HS256' })\n .setIssuer(this.clientId)\n .setSubject(this.clientId)\n .setAudience(tokenEndpoint)\n .setJti(crypto.randomUUID())\n .setIssuedAt()\n .setExpirationTime('5m')\n .sign(secret);\n }\n\n private async exchangeToken(clientAssertion: string): Promise<string> {\n const tokenData = await this.exchangeTokenForServer(clientAssertion);\n\n // Calculate token expiry time (current time + expires_in seconds - 60 seconds buffer)\n const expiresInSeconds = tokenData.expires_in || 3600; // Default to 1 hour if not provided\n this.tokenExpiry = Date.now() + (expiresInSeconds - 60) * 1000; // 60 seconds buffer\n\n return tokenData.access_token;\n }\n\n private async exchangeTokenForServer(clientAssertion: string): Promise<any> {\n const tokenEndpoint = urlJoin(this.oauthBaseUrl, 'token');\n log('Exchanging token at endpoint: %s', tokenEndpoint);\n\n const params = new URLSearchParams();\n params.append('grant_type', 'client_credentials');\n params.append(\n 'client_assertion_type',\n 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',\n );\n params.append('client_assertion', clientAssertion);\n\n const response = await fetch(tokenEndpoint, {\n body: params,\n headers: { 'Content-Type': 'application/x-www-form-urlencoded' },\n method: 'POST',\n });\n\n const tokenData = await response.json();\n\n if (!response.ok) {\n throw new Error(`Token exchange failed: ${JSON.stringify(tokenData)}`);\n }\n\n return tokenData;\n }\n\n private async setM2MAuthToken(): Promise<void> {\n // Use shared token state if available\n const currentAccessToken = this.sharedTokenState?.accessToken || this.accessToken;\n const currentTokenExpiry = this.sharedTokenState?.tokenExpiry || this.tokenExpiry;\n\n log(\n 'Token check: hasSharedState=%s, hasCurrentToken=%s, tokenExpiry=%d, currentTime=%d',\n !!this.sharedTokenState,\n !!currentAccessToken,\n currentTokenExpiry || 0,\n Date.now(),\n );\n\n // Check if we have a valid token that hasn't expired\n if (currentAccessToken && currentTokenExpiry && Date.now() < currentTokenExpiry) {\n log(\n 'Using existing M2M access token (expires in %d seconds)',\n Math.floor((currentTokenExpiry - Date.now()) / 1000),\n );\n this.headers.Authorization = `Bearer ${currentAccessToken}`;\n return;\n }\n\n // Check if there's already a token request in progress (for shared state)\n if (this.sharedTokenState?.tokenPromise) {\n log('Token request already in progress, waiting for completion...');\n const token = await this.sharedTokenState.tokenPromise;\n this.headers.Authorization = `Bearer ${token}`;\n log('Using token from concurrent request');\n return;\n }\n\n log('Fetching new M2M access token...');\n\n // Create token request promise and store it in shared state\n const tokenPromise = this.fetchNewToken();\n if (this.sharedTokenState) {\n this.sharedTokenState.tokenPromise = tokenPromise;\n }\n\n try {\n const newAccessToken = await tokenPromise;\n\n // Update both local and shared token state\n this.accessToken = newAccessToken;\n this.headers.Authorization = `Bearer ${newAccessToken}`;\n\n if (this.sharedTokenState) {\n this.sharedTokenState.accessToken = newAccessToken;\n this.sharedTokenState.tokenExpiry = this.tokenExpiry;\n // Clear the promise since we're done\n this.sharedTokenState.tokenPromise = undefined;\n log(\n 'Updated shared token state (expires in %d seconds)',\n Math.floor(((this.tokenExpiry || 0) - Date.now()) / 1000),\n );\n }\n\n log('Successfully set new M2M access token');\n } catch (error) {\n // Clear the promise on error\n if (this.sharedTokenState) {\n this.sharedTokenState.tokenPromise = undefined;\n }\n throw error;\n }\n }\n\n private async fetchNewToken(): Promise<string> {\n const assertion = await this.createClientAssertion();\n return await this.exchangeToken(assertion);\n }\n}\n","import {\n InstallFailureAnalysis,\n InstallFailureAnalysisQuery,\n RangeQuery,\n RangeStats,\n TopPlugin,\n TopPluginsQuery,\n} from '@lobehub/market-types';\nimport debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin:analysis');\n\n/**\n * Market Overview Statistics Interface\n * Defines the structure for market overview data\n */\nexport interface MarketOverviewStats {\n devices: {\n count: number;\n prevCount: number;\n };\n installs: {\n count: number;\n prevCount: number;\n };\n period: string;\n pluginCalls: {\n count: number;\n prevCount: number;\n };\n plugins: {\n count: number;\n prevCount: number;\n };\n}\n\n/**\n * Period type for analysis queries\n */\nexport type AnalysisPeriod = '1d' | '7d' | '30d' | '1mo' | '3mo' | '1y';\n\n/**\n * Market overview query parameters\n */\nexport interface MarketOverviewQuery {\n /** Analysis period: 1d, 7d, 30d (rolling periods) or 1mo, 3mo, 1y (natural periods) */\n period?: AnalysisPeriod;\n}\n\n/**\n * Standard API response wrapper\n */\ninterface ApiResponse<T> {\n data: T;\n success: boolean;\n}\n\n/**\n * Analysis Management Service\n *\n * Provides administrative functionality for accessing market analysis and statistics.\n * This service handles retrieving various analytics reports including market overview,\n * plugin trends, and installation analytics for administrative dashboards.\n */\nexport class AnalysisService extends BaseSDK {\n /**\n * Retrieves market overview statistics\n *\n * Returns comprehensive market statistics including plugin counts,\n * installation metrics, new plugin trends, and rating averages\n * with comparison to previous periods.\n *\n * @param params - Query parameters for the analysis\n * @returns Promise resolving to market overview statistics\n */\n async getMarketOverview(params: MarketOverviewQuery = {}): Promise<MarketOverviewStats> {\n const { period = '30d' } = params;\n\n log('Getting market overview statistics for period: %s', period);\n\n const searchParams = new URLSearchParams();\n if (period) {\n searchParams.append('period', period);\n }\n\n const url = `/admin/analysis/plugin/overview${searchParams.toString() ? `?${searchParams.toString()}` : ''}`;\n\n const result = await this.request<ApiResponse<MarketOverviewStats>>(url);\n\n log('Market overview statistics retrieved successfully: %s', result.data);\n\n return result.data;\n }\n\n /**\n * Retrieves market overview statistics for 1 day period\n *\n * Convenience method for getting daily market statistics.\n *\n * @returns Promise resolving to market overview statistics for 1 day\n */\n async getDailyOverview(): Promise<MarketOverviewStats> {\n log('Getting daily market overview');\n return this.getMarketOverview({ period: '1d' });\n }\n\n /**\n * Retrieves market overview statistics for 7 days period\n *\n * Convenience method for getting weekly market statistics.\n *\n * @returns Promise resolving to market overview statistics for 7 days\n */\n async getWeeklyOverview(): Promise<MarketOverviewStats> {\n log('Getting weekly market overview');\n return this.getMarketOverview({ period: '7d' });\n }\n\n /**\n * Retrieves market overview statistics for 30 days period\n *\n * Convenience method for getting monthly market statistics.\n *\n * @returns Promise resolving to market overview statistics for 30 days\n */\n async getMonthlyOverview(): Promise<MarketOverviewStats> {\n log('Getting monthly market overview');\n return this.getMarketOverview({ period: '30d' });\n }\n\n /**\n * Retrieves market overview statistics for current natural month\n *\n * Convenience method for getting current month vs previous month statistics.\n *\n * @returns Promise resolving to market overview statistics for current month\n */\n async getThisMonthOverview(): Promise<MarketOverviewStats> {\n log('Getting this month market overview');\n return this.getMarketOverview({ period: '1mo' });\n }\n\n /**\n * Retrieves market overview statistics for current quarter\n *\n * Convenience method for getting current quarter vs previous quarter statistics.\n *\n * @returns Promise resolving to market overview statistics for current quarter\n */\n async getQuarterlyOverview(): Promise<MarketOverviewStats> {\n log('Getting quarterly market overview');\n return this.getMarketOverview({ period: '3mo' });\n }\n\n /**\n * Retrieves market overview statistics for current year\n *\n * Convenience method for getting current year vs previous year statistics.\n *\n * @returns Promise resolving to market overview statistics for current year\n */\n async getYearlyOverview(): Promise<MarketOverviewStats> {\n log('Getting yearly market overview');\n return this.getMarketOverview({ period: '1y' });\n }\n\n /**\n * Retrieves install failure analysis for plugins within a date range\n *\n * Returns detailed analysis of plugin installation failures including failure counts,\n * failure rates, and most common error messages for each plugin with failures.\n *\n * @param params - Query parameters for the failure analysis\n * @returns Promise resolving to install failure analysis data\n */\n async getInstallFailureAnalysis(\n params: InstallFailureAnalysisQuery,\n ): Promise<InstallFailureAnalysis[]> {\n const { range, limit = 15 } = params;\n\n log('Getting install failure analysis for range: %o, limit: %d', range, limit);\n\n const searchParams = new URLSearchParams();\n searchParams.append('range', JSON.stringify(range));\n if (limit !== 15) {\n searchParams.append('limit', limit.toString());\n }\n\n const url = `/admin/analysis/plugin/install-failure?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<InstallFailureAnalysis[]>>(url);\n\n log('Install failure analysis retrieved successfully for %d plugins', result.data.length);\n\n return result.data;\n }\n\n /**\n * Calculates growth rate between current and previous values\n *\n * Utility method for calculating percentage growth rates from the statistics.\n *\n * @param current - Current period value\n * @param previous - Previous period value\n * @returns Growth rate as percentage (e.g., 15.5 for 15.5% growth)\n */\n static calculateGrowthRate(current: number, previous: number): number {\n if (previous === 0) return current > 0 ? 100 : 0;\n return Math.round(((current - previous) / previous) * 100 * 10) / 10;\n }\n\n /**\n * Formats market overview statistics with calculated growth rates\n *\n * Utility method that enhances the raw statistics with calculated growth rates\n * for easier consumption in dashboards and reports.\n *\n * @param stats - Raw market overview statistics\n * @returns Enhanced statistics with growth rate calculations\n */\n static formatMarketOverview(stats: MarketOverviewStats) {\n return {\n ...stats,\n growth: {\n devices: this.calculateGrowthRate(stats.devices.count, stats.devices.prevCount),\n installs: this.calculateGrowthRate(stats.installs.count, stats.installs.prevCount),\n pluginCalls: this.calculateGrowthRate(stats.pluginCalls.count, stats.pluginCalls.prevCount),\n plugins: this.calculateGrowthRate(stats.plugins.count, stats.plugins.prevCount),\n },\n };\n }\n\n /**\n * Retrieves installation trend statistics for a specified date range\n *\n * Returns daily installation counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to installation trend statistics\n */\n async getRangeInstalls(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting installation trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-installs?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Installation trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves plugin growth trend statistics for a specified date range\n *\n * Returns daily plugin creation counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to plugin growth trend statistics\n */\n async getRangePlugins(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting plugin growth trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-plugins?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Plugin growth trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves device growth trend statistics for a specified date range\n *\n * Note: This is a system-level statistic that tracks device registrations,\n * not plugin-specific metrics. It provides daily device registration counts\n * and trends for the specified period with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to device growth trend statistics\n */\n async getRangeDevices(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting device growth trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/system/range-devices?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Device growth trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Retrieves plugin call trend statistics for a specified date range\n *\n * Returns daily plugin call counts and trends for the specified period\n * with optional comparison to a previous period.\n *\n * @param params - Query parameters including date range and display config\n * @returns Promise resolving to plugin call trend statistics\n */\n async getRangeCalls(params: RangeQuery): Promise<RangeStats> {\n const { display, range, prevRange } = params;\n\n log('Getting plugin call trend statistics for range: %s to %s', range[0], range[1]);\n\n const searchParams = new URLSearchParams();\n searchParams.append('display', display);\n searchParams.append('range', range.join(','));\n if (prevRange) {\n searchParams.append('prevRange', prevRange.join(','));\n }\n\n const url = `/admin/analysis/plugin/range-calls?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<RangeStats>>(url);\n\n log(\n 'Plugin call trend statistics retrieved successfully: %d data points',\n result.data.data.length,\n );\n\n return result.data;\n }\n\n /**\n * Calculates trend growth rate between current and previous period totals\n *\n * Utility method for calculating percentage growth rates from range statistics.\n *\n * @param stats - Range statistics with sum and prevSum\n * @returns Growth rate as percentage (e.g., 15.5 for 15.5% growth)\n */\n static calculateTrendGrowthRate(stats: RangeStats): number {\n return this.calculateGrowthRate(stats.sum, stats.prevSum);\n }\n\n /**\n * Retrieves top plugins sorted by specified criteria within a date range\n *\n * Returns list of plugins sorted by the specified criteria (installs or calls)\n * in descending order for the specified date range.\n *\n * @param params - Query parameters including date range, sort criteria, and limit\n * @returns Promise resolving to list of top plugins\n */\n async getTopPlugins(params: TopPluginsQuery): Promise<TopPlugin[]> {\n const { range, sortBy = 'installs', limit = 10 } = params;\n\n const searchParams = new URLSearchParams();\n searchParams.append('range', range.join(','));\n searchParams.append('sortBy', sortBy);\n searchParams.append('limit', limit.toString());\n\n const url = `/admin/analysis/plugin/top-plugins?${searchParams.toString()}`;\n\n const result = await this.request<ApiResponse<TopPlugin[]>>(url);\n\n return result.data;\n }\n}\n","import type {\n AdminDeploymentOption,\n AdminPluginItem,\n AdminPluginItemDetail,\n IncompleteI18nPlugin,\n InstallationDetails,\n PluginManifest,\n PluginVersion,\n PluginVersionLocalization,\n SystemDependency,\n} from '@lobehub/market-types';\nimport debug from 'debug';\n\nimport { BaseSDK } from '@/core/BaseSDK';\nimport {\n AdminListQueryParams,\n AdminListResponse,\n PluginI18nImportParams,\n PluginI18nImportResponse,\n PluginUpdateParams,\n PluginVersionCreateParams,\n PluginVersionUpdateParams,\n UnclaimedPluginItem,\n} from '@/types';\n\n// Create debug instance for logging\nconst log = debug('lobe-market-sdk:admin:plugins');\n\n/**\n * Plugin Management Service\n *\n * Provides administrative functionality for managing plugins in the marketplace.\n * This service handles CRUD operations for plugins, plugin versions, and deployment options.\n */\nexport class PluginService extends BaseSDK {\n /**\n * Batch imports plugin manifests using the dedicated import endpoint\n *\n * This method is intended for use with scripts and bulk import operations.\n *\n * @param manifests - Array of plugin manifests to import\n * @param ownerId - Optional owner ID to associate with the imported plugins\n * @returns Promise resolving to the import results with counts of success, skipped, failed, and a list of failed IDs\n */\n async importPlugins(\n manifests: PluginManifest[],\n ownerId?: number,\n ): Promise<{ failed: number; failedIds: string[]; skipped: number; success: number }> {\n log(`Starting batch plugin import of ${manifests.length} manifests`);\n if (ownerId) {\n log(`Using specified owner ID for import: ${ownerId}`);\n }\n\n const response = await this.request<{\n data: { failed: number; failedIds: string[]; skipped: number; success: number };\n }>('/admin/plugins/import', {\n body: JSON.stringify({\n manifests,\n ownerId,\n }),\n method: 'POST',\n });\n\n log(\n `Plugin import completed: ${response.data.success} succeeded, ${response.data.skipped} skipped, ${response.data.failed} failed`,\n );\n return response.data;\n }\n\n /**\n * Imports plugin internationalization (i18n) data\n *\n * Allows importing localized content for a specific plugin version.\n * This method creates or updates localizations for the specified plugin.\n *\n * @param params - Plugin i18n import parameters containing identifier, version, and localizations\n * @returns Promise resolving to the import results with counts of success and failure\n */\n async importPluginI18n(params: PluginI18nImportParams): Promise<PluginI18nImportResponse> {\n log(\n `Starting i18n import for plugin ${params.identifier} v${params.version} with ${params.localizations.length} localizations`,\n );\n\n const response = await this.request<{\n data: PluginI18nImportResponse;\n message: string;\n }>('/admin/plugins/import/i18n', {\n body: JSON.stringify(params),\n method: 'POST',\n });\n\n log(\n `Plugin i18n import completed: ${response.data.success} succeeded, ${response.data.failed} failed, ${response.data.totalLocalizations} total`,\n );\n return response.data;\n }\n\n /**\n * Retrieves a list of plugins with admin details\n *\n * Supports filtering, pagination, and sorting of results.\n *\n * @param params - Query parameters for filtering and pagination\n * @returns Promise resolving to the plugin list response with admin details\n */\n async getPlugins(params: AdminListQueryParams = {}): Promise<AdminListResponse<AdminPluginItem>> {\n log('Getting plugins with params: %O', params);\n\n const queryString = this.buildQueryString(params);\n const url = `/admin/plugins${queryString}`;\n\n const result = await this.request<AdminListResponse<AdminPluginItem>>(url);\n\n log('Retrieved %d plugins', result.data.length);\n return result;\n }\n\n /**\n * Retrieves all published plugin identifiers\n *\n * Returns a lightweight list of all published plugin identifiers without\n * full plugin metadata. This is useful for admin operations that need to know\n * which plugins are currently published and available to users.\n *\n * @returns Promise resolving to an array containing identifiers array and last modified time\n */\n async getPublishedIdentifiers(): Promise<{ identifier: string; lastModified: string }[]> {\n log('Getting published plugin identifiers (admin)');\n\n const result =\n await this.request<{ identifier: string; lastModified: string }[]>('/v1/plugins/identifiers');\n log('Retrieved %d published plugin identifiers (admin)', result.length);\n return result;\n }\n\n /**\n * Retrieves a single plugin with full admin details\n *\n * @param id - Plugin ID or identifier\n * @returns Promise resolving to the detailed plugin information with version history\n */\n async getPlugin(id: number | string): Promise<AdminPluginItemDetail> {\n log('Getting plugin details (admin): %d', id);\n\n const result = await this.request<AdminPluginItemDetail>(`/admin/plugins/${id}`);\n log('Retrieved plugin with %d versions', result.versions.length);\n return result;\n }\n\n /**\n * Retrieves a plugin by its GitHub repository URL\n *\n * @param githubUrl - The GitHub repository URL to search for\n * @returns Promise resolving to the detailed plugin information with version history\n */\n async getPluginByGithubUrl(githubUrl: string): Promise<AdminPluginItemDetail> {\n log('Getting plugin by GitHub URL: %s', githubUrl);\n\n const queryString = this.buildQueryString({ url: githubUrl });\n const result = await this.request<AdminPluginItemDetail>(\n `/admin/plugins/by-github-url${queryString}`,\n );\n log('Retrieved plugin with %d versions', result.versions.length);\n return result;\n }\n\n /**\n * Updates plugin information\n *\n * @param id - Plugin ID\n * @param data - Plugin update data containing fields to update\n * @returns Promise resolving to the updated plugin\n */\n async updatePlugin(id: number, data: PluginUpdateParams): Promise<AdminPluginItem> {\n log('Updating plugin: %d, data: %O', id, data);\n\n const result = await this.request<AdminPluginItem>(`/admin/plugins/${id}`, {\n body: JSON.stringify(data),\n method: 'PUT',\n });\n log('Plugin updated successfully');\n return result;\n }\n\n /**\n * Updates plugin publication status\n *\n * @param id - Plugin ID\n * @param status - New status to set\n * @returns Promise resolving to success response\n */\n async updatePluginStatus(\n id: number,\n status: 'published' | 'draft' | 'review' | 'rejected',\n ): Promise<{ message: string; success: boolean }> {\n log('Updating plugin status: %d to %s', id, status);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${id}/status`,\n {\n body: JSON.stringify({ status }),\n method: 'PATCH',\n },\n );\n log('Plugin status updated successfully');\n return result;\n }\n\n /**\n * Deletes a plugin\n *\n * @param id - Plugin ID\n * @returns Promise resolving to success response\n */\n async deletePlugin(id: number): Promise<{ message: string; success: boolean }> {\n log('Deleting plugin: %d', id);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${id}`,\n { method: 'DELETE' },\n );\n log('Plugin deleted successfully');\n return result;\n }\n\n /**\n * Retrieves the version history for a plugin\n *\n * @param pluginId - Plugin ID\n * @returns Promise resolving to an array of plugin versions\n */\n async getPluginVersions(pluginId: number): Promise<PluginVersion[]> {\n log('Getting plugin versions: pluginId=%d', pluginId);\n\n const result = await this.request<PluginVersion[]>(`/admin/plugins/${pluginId}/versions`);\n\n log('Retrieved %d versions', result.length);\n return result;\n }\n\n /**\n * Retrieves a specific plugin version\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @returns Promise resolving to the plugin version details\n */\n async getPluginVersion(pluginId: number, versionId: number): Promise<PluginVersion> {\n log('Getting version details: pluginId=%d, versionId=%d', pluginId, versionId);\n\n const result = await this.request<PluginVersion>(\n `/admin/plugins/${pluginId}/versions/${versionId}`,\n );\n log('Version details retrieved');\n return result;\n }\n\n /**\n * Retrieves all localizations for a specific plugin version\n *\n * @param pluginId - Plugin ID or identifier\n * @param versionId - Version ID\n * @returns Promise resolving to an array of plugin version localizations\n */\n async getPluginLocalizations(\n pluginId: number | string,\n versionId: number,\n ): Promise<PluginVersionLocalization[]> {\n log('Getting localizations: pluginId=%s, versionId=%d', pluginId, versionId);\n\n const result = await this.request<PluginVersionLocalization[]>(\n `/admin/plugins/${pluginId}/versions/${versionId}/localizations`,\n );\n log('Retrieved %d localizations for plugin %s, version %d', result.length, pluginId, versionId);\n return result;\n }\n\n /**\n * Creates a new plugin version with all version-specific data\n *\n * @param pluginId - Plugin ID\n * @param data - Version creation data including all version-specific fields\n * @returns Promise resolving to the created plugin version\n */\n async createPluginVersion(\n pluginId: number,\n data: PluginVersionCreateParams,\n ): Promise<PluginVersion> {\n log('Creating new plugin version: pluginId=%d, data: %O', pluginId, data);\n\n const result = await this.request<PluginVersion>(`/admin/plugins/${pluginId}/versions`, {\n body: JSON.stringify(data),\n method: 'POST',\n });\n log('Plugin version created successfully: version=%s', data.version);\n return result;\n }\n\n /**\n * Creates a new plugin version from a manifest\n * Extracts version data from the manifest for easier creation\n *\n * @param pluginId - Plugin ID\n * @param manifest - Plugin manifest containing all version data\n * @param options - Additional options for version creation\n * @returns Promise resolving to the created plugin version\n */\n async createPluginVersionFromManifest(\n pluginId: number,\n manifest: PluginManifest,\n options: {\n isLatest?: boolean;\n isValidated?: boolean;\n } = {},\n ): Promise<PluginVersion> {\n log(\n 'Creating plugin version from manifest: pluginId=%d, version=%s',\n pluginId,\n manifest.version,\n );\n\n const versionData: PluginVersionCreateParams = {\n author: manifest.author?.name,\n authorUrl: manifest.author?.url,\n capabilitiesPrompts: manifest.capabilities?.prompts,\n capabilitiesResources: manifest.capabilities?.resources,\n capabilitiesTools: manifest.capabilities?.tools,\n category: manifest.category,\n description: manifest.description,\n icon: manifest.icon,\n isLatest: options.isLatest,\n isValidated: options.isValidated,\n name: manifest.name,\n prompts: manifest.prompts,\n readme: manifest.overview?.readme,\n resources: manifest.resources,\n summary: manifest.overview?.summary,\n tags: manifest.tags,\n tools: manifest.tools,\n version: manifest.version,\n };\n\n return this.createPluginVersion(pluginId, versionData);\n }\n\n /**\n * Updates a plugin version with comprehensive version-specific data\n *\n * @param idOrIdentifier - Plugin ID\n * @param versionId - Version ID\n * @param data - Version update data including all version-specific fields\n * @returns Promise resolving to the updated plugin version\n */\n async updatePluginVersion(\n idOrIdentifier: number | string,\n versionId: number,\n data: PluginVersionUpdateParams,\n ): Promise<PluginVersion> {\n log(\n 'Updating plugin version: pluginId=%d, versionId=%d, data: %O',\n idOrIdentifier,\n versionId,\n data,\n );\n\n const result = await this.request<PluginVersion>(\n `/admin/plugins/${idOrIdentifier}/versions/${versionId}`,\n {\n body: JSON.stringify(data),\n method: 'PUT',\n },\n );\n\n log('Plugin version updated successfully');\n return result;\n }\n\n /**\n * Deletes a plugin version\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @returns Promise resolving to success response\n */\n async deletePluginVersion(\n pluginId: number,\n versionId: number,\n ): Promise<{ message: string; success: boolean }> {\n log('Deleting version: pluginId=%d, versionId=%d', pluginId, versionId);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${pluginId}/versions/${versionId}`,\n { method: 'DELETE' },\n );\n log('Plugin version deleted successfully');\n return result;\n }\n\n /**\n * Sets a specific version as the latest version of a plugin\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID to set as latest\n * @returns Promise resolving to success response\n */\n async setPluginVersionAsLatest(\n pluginId: number,\n versionId: number,\n ): Promise<{ message: string; success: boolean }> {\n log('Setting version as latest: pluginId=%d, versionId=%d', pluginId, versionId);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${pluginId}/versions/${versionId}/latest`,\n { method: 'POST' },\n );\n log('Version set as latest successfully');\n return result;\n }\n\n /**\n * Updates plugin visibility\n *\n * @param id - Plugin ID\n * @param visibility - New visibility setting\n * @returns Promise resolving to success response\n */\n async updatePluginVisibility(\n id: number,\n visibility: 'public' | 'private' | 'unlisted',\n ): Promise<{ message: string; success: boolean }> {\n log('Updating plugin visibility: %d to %s', id, visibility);\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${id}/visibility`,\n {\n body: JSON.stringify({ visibility }),\n method: 'PATCH',\n },\n );\n log('Plugin visibility updated successfully');\n return result;\n }\n\n /**\n * Updates status for multiple plugins in a single operation\n *\n * @param ids - Array of plugin IDs to update\n * @param status - New status to set for all specified plugins\n * @returns Promise resolving to success response\n */\n async batchUpdatePluginStatus(\n ids: number[],\n status: 'published' | 'draft' | 'review' | 'rejected',\n ): Promise<{ message: string; success: boolean }> {\n log('Batch updating plugin status: %O to %s', ids, status);\n\n const result = await this.request<{ message: string; success: boolean }>(\n '/admin/plugins/batch/status',\n {\n body: JSON.stringify({ ids, status }),\n method: 'PATCH',\n },\n );\n log('Batch plugin status update completed');\n return result;\n }\n\n /**\n * Deletes multiple plugins in a single operation\n *\n * @param ids - Array of plugin IDs to delete\n * @returns Promise resolving to success response\n */\n async batchDeletePlugins(ids: number[]): Promise<{ message: string; success: boolean }> {\n log('Batch deleting plugins: %O', ids);\n\n const result = await this.request<{ message: string; success: boolean }>(\n '/admin/plugins/batch/delete',\n {\n body: JSON.stringify({ ids }),\n method: 'DELETE',\n },\n );\n log('Batch plugin deletion completed');\n return result;\n }\n\n /**\n * Retrieves detailed information about a plugin version including deployment options\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @returns Promise resolving to version details with deployment options\n */\n async getPluginVersionDetails(\n pluginId: number,\n versionId: number,\n ): Promise<PluginVersion & { deploymentOptions: any }> {\n log(\n 'Getting version details with deployment options: pluginId=%d, versionId=%d',\n pluginId,\n versionId,\n );\n\n const result = await this.request<PluginVersion & { deploymentOptions: any }>(\n `/admin/plugins/${pluginId}/versions/${versionId}`,\n );\n log('Version details with deployment options retrieved');\n return result;\n }\n\n /**\n * Retrieves deployment options for a specific plugin version\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @returns Promise resolving to an array of deployment options\n */\n async getDeploymentOptions(\n pluginId: number,\n versionId: number,\n ): Promise<AdminDeploymentOption[]> {\n log('Getting deployment options: pluginId=%d, versionId=%d', pluginId, versionId);\n\n const result = await this.request<AdminDeploymentOption[]>(\n `/admin/plugins/${pluginId}/versions/${versionId}/deployment-options`,\n );\n log('Retrieved %d deployment options', result.length);\n return result;\n }\n\n /**\n * Creates a new deployment option for a plugin version\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @param data - Deployment option configuration data\n * @returns Promise resolving to the created deployment option\n */\n async createDeploymentOption(\n pluginId: number,\n versionId: number,\n data: {\n connectionArgs?: string[];\n connectionCommand?: string;\n connectionType: string;\n description?: string;\n installationDetails?: InstallationDetails;\n installationMethod: string;\n isRecommended?: boolean;\n },\n ): Promise<AdminDeploymentOption> {\n log('Creating deployment option: pluginId=%d, versionId=%d', pluginId, versionId);\n\n const result = await this.request<AdminDeploymentOption>(\n `/admin/plugins/${pluginId}/versions/${versionId}/deployment-options`,\n {\n body: JSON.stringify(data),\n method: 'POST',\n },\n );\n log('Deployment option created successfully');\n return result;\n }\n\n /**\n * Updates an existing deployment option\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @param optionId - Deployment option ID\n * @param data - Updated deployment option configuration\n * @returns Promise resolving to the updated deployment option\n */\n async updateDeploymentOption(\n pluginId: number,\n versionId: number,\n optionId: number,\n data: {\n connectionArgs?: string[];\n connectionCommand?: string;\n connectionType?: string;\n description?: string;\n installationDetails?: Record<string, any>;\n installationMethod?: string;\n isRecommended?: boolean;\n },\n ): Promise<AdminDeploymentOption> {\n log(\n 'Updating deployment option: pluginId=%d, versionId=%d, optionId=%d',\n pluginId,\n versionId,\n optionId,\n );\n\n const result = await this.request<AdminDeploymentOption>(\n `/admin/plugins/${pluginId}/versions/${versionId}/deployment-options/${optionId}`,\n {\n body: JSON.stringify(data),\n method: 'PUT',\n },\n );\n log('Deployment option updated successfully');\n return result;\n }\n\n /**\n * Deletes a deployment option\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @param optionId - Deployment option ID\n * @returns Promise resolving to success response\n */\n async deleteDeploymentOption(\n pluginId: number,\n versionId: number,\n optionId: number,\n ): Promise<{ message: string; success: boolean }> {\n log(\n 'Deleting deployment option: pluginId=%d, versionId=%d, optionId=%d',\n pluginId,\n versionId,\n optionId,\n );\n\n const result = await this.request<{ message: string; success: boolean }>(\n `/admin/plugins/${pluginId}/versions/${versionId}/deployment-options/${optionId}`,\n { method: 'DELETE' },\n );\n log('Deployment option deleted successfully');\n return result;\n }\n\n /**\n * Retrieves system dependencies for a deployment option\n *\n * @param pluginId - Plugin ID\n * @param versionId - Version ID\n * @param optionId - Deployment option ID\n * @returns Promise resolving to an array of system dependencies\n */\n async getDeploymentOptionSystemDependencies(\n pluginId: number,\n versionId: number,\n optionId: number,\n ): Promise<SystemDependency[]> {\n log(\n 'Getting system dependencies: pluginId=%d, versionId=%d, optionId=%d',\n pluginId,\n versionId,\n optionId,\n );\n\n const result = await this.req