import { getInjectedPlatformContext, resolveCurrentExperimentId, } from './platformContextHelpers' import type { BuildPluginEndpointUrlOptions, PluginContract, PluginEndpointDefinition, PluginEndpointParamDefinition, } from './usePluginClient' function normalizeApiBaseUrl(baseUrl: string | undefined): string | undefined { if (!baseUrl) return undefined return baseUrl.length > 1 ? baseUrl.replace(/\/+$/, '') : baseUrl } function normalizeApiPrefix(prefix: string | undefined): string | undefined { if (!prefix) return undefined if (prefix.startsWith('/api/')) return normalizeApiBaseUrl(prefix) if (prefix === '/api') return prefix if (prefix.startsWith('/')) return normalizeApiBaseUrl(`/api${prefix}`) return normalizeApiBaseUrl(`/api/${prefix}`) } function endpointParamName(param: PluginEndpointParamDefinition): string { return typeof param === 'string' ? param : param.name } function endpointParamFieldName(param: PluginEndpointParamDefinition): string { return typeof param === 'string' ? param : param.fieldName ?? param.name } function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') } function replacePathParam(path: string, name: string, encoded: string): string { const escapedName = escapeRegExp(name) return path .replace(new RegExp(`\\{${escapedName}(?::[^{}]+)?\\}`, 'g'), () => encoded) .replace(new RegExp(`:${escapedName}(?=/|$)`, 'g'), () => encoded) } /** Resolve the runtime plugin API base URL from env, explicit options, platform injection, or contract metadata. */ export function resolvePluginBaseUrl(contract: PluginContract, explicitBaseUrl?: string): string { const envPrefix = (import.meta.env?.VITE_API_PREFIX as string | undefined) || undefined if (envPrefix) return normalizeApiBaseUrl(envPrefix) ?? envPrefix if (explicitBaseUrl) return normalizeApiBaseUrl(explicitBaseUrl) ?? explicitBaseUrl const injected = getInjectedPlatformContext() const platformPrefix = normalizeApiBaseUrl(injected?.plugin?.api_prefix) || normalizeApiPrefix(injected?.plugin?.route_prefix) if (platformPrefix) return platformPrefix return normalizeApiBaseUrl(contract.plugin.apiPrefix) || '/api' } function encodePath( path: string, payload: Record, pathParams: PluginEndpointParamDefinition[], ): string { let nextPath = path for (const param of pathParams) { const name = endpointParamName(param) const fieldName = endpointParamFieldName(param) const value = payload[fieldName] ?? payload[name] if (value === undefined || value === null) { throw new Error(`[MINT SDK] Missing path parameter '${fieldName}' for plugin endpoint ${path}`) } const encoded = encodeURIComponent(String(value)) nextPath = replacePathParam(nextPath, name, encoded) if (fieldName !== name) { nextPath = replacePathParam(nextPath, fieldName, encoded) } } return nextPath } function withDefaultPathParams( payload: Record, pathParams: PluginEndpointParamDefinition[], ): Record { if (!pathParams.length) return payload const nextPayload = { ...payload } for (const param of pathParams) { const name = endpointParamName(param) const fieldName = endpointParamFieldName(param) if ( (nextPayload[fieldName] !== undefined && nextPayload[fieldName] !== null) || (nextPayload[name] !== undefined && nextPayload[name] !== null) ) { continue } if (isExperimentPathParam(fieldName) || isExperimentPathParam(name)) { const experimentId = resolveCurrentExperimentId() if (experimentId !== undefined) { nextPayload[fieldName] = experimentId } } } return nextPayload } function isExperimentPathParam(param: string): boolean { return param === 'experimentId' || param === 'experiment_id' } function withoutPathParams(payload: Record, pathParams: PluginEndpointParamDefinition[]) { const copy = { ...payload } for (const param of pathParams) { delete copy[endpointParamFieldName(param)] delete copy[endpointParamName(param)] } return copy } function queryParamsFromPayload( payload: Record, pathParams: PluginEndpointParamDefinition[], queryParams?: PluginEndpointParamDefinition[], endpointPath?: string, includeUnspecifiedParams = false, ) { if (!queryParams?.length) { return includeUnspecifiedParams ? withoutPathParams(payload, pathParams) : {} } const query: Record = {} for (const param of queryParams) { const name = typeof param === 'string' ? param : param.name const fieldName = typeof param === 'string' ? param : param.fieldName ?? param.name const required = typeof param !== 'string' && param.required === true const value = payload[fieldName] if (value === undefined || value === null) { if (required) { throw new Error( `[MINT SDK] Missing query parameter '${fieldName}' for plugin endpoint ${endpointPath ?? ''}`, ) } continue } query[name] = value } return query } export function hasKeys(value: Record): boolean { return Object.keys(value).length > 0 } function isPlainPayloadObject(value: unknown): value is Record { return !!value && typeof value === 'object' && !Array.isArray(value) } function hasOwn(value: Record, key: string): boolean { return Object.prototype.hasOwnProperty.call(value, key) } function normalizeStructuredEndpointPayload(payload: Record): Record { const pathParamPayload = payload.pathParams const queryPayload = payload.query const hasStructuredPathParams = hasOwn(payload, 'pathParams') && isPlainPayloadObject(pathParamPayload) const hasStructuredQuery = hasOwn(payload, 'query') && isPlainPayloadObject(queryPayload) if (!hasStructuredPathParams && !hasStructuredQuery) return payload const nextPayload = { ...payload } delete nextPayload.pathParams delete nextPayload.query if (hasStructuredPathParams) { Object.assign(nextPayload, pathParamPayload) } if (hasStructuredQuery) { Object.assign(nextPayload, queryPayload) } return nextPayload } export function requestBodyFromPayload( payload: unknown, requestPayload: Record, endpoint: PluginEndpointDefinition, ): unknown { if (!endpoint.hasBody) return undefined const body = endpoint.pathParams?.length || endpoint.queryParams?.length ? requestPayload.body : payload if (body === undefined || body === null) { throw new Error(`[MINT SDK] Missing request body for plugin endpoint ${endpoint.path}`) } return body } export function pluginEndpointRequestParts( contract: PluginContract, endpoint: PluginEndpointDefinition, payload?: unknown, explicitBaseUrl?: string, ) { const payloadObject = isPlainPayloadObject(payload) ? normalizeStructuredEndpointPayload(payload) : {} const pathParams = endpoint.pathParams ?? [] const requestPayload = withDefaultPathParams(payloadObject, pathParams) const path = encodePath(endpoint.path, requestPayload, pathParams) const query = queryParamsFromPayload( requestPayload, pathParams, endpoint.queryParams, endpoint.path, endpoint.method === 'get', ) return { baseUrl: resolvePluginBaseUrl(contract, explicitBaseUrl), path, query, requestPayload, } } function appendQueryValue(params: URLSearchParams, key: string, value: unknown): void { if (value === undefined || value === null) return if (Array.isArray(value)) { for (const item of value) { appendQueryValue(params, key, item) } return } if (typeof value === 'object') { params.append(key, JSON.stringify(value)) return } params.append(key, String(value)) } function queryString(query: Record): string { const params = new URLSearchParams() for (const [key, value] of Object.entries(query)) { appendQueryValue(params, key, value) } const serialized = params.toString() return serialized ? `?${serialized}` : '' } function joinBaseUrl(baseUrl: string, path: string): string { if (!baseUrl) return path const normalizedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl const normalizedPath = path.startsWith('/') ? path : `/${path}` return `${normalizedBase}${normalizedPath}` } /** Build the concrete URL for a generated plugin endpoint without making a request. */ export function buildPluginEndpointUrl( contract: PluginContract, endpoint: PluginEndpointDefinition, payload?: unknown, options: BuildPluginEndpointUrlOptions = {}, ): string { const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl) const pathWithQuery = `${parts.path}${queryString(parts.query)}` if (options.includeBaseUrl === false) return pathWithQuery return joinBaseUrl(parts.baseUrl, pathWithQuery) }