import { MINDSPACE_SERVER_ADAPTER_BINDINGS, MINDSPACE_SERVER_ADAPTER_BINDING_KEYS, MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION, MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS, MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES, } from './mindspace-server-adapter-contract.mjs'; function trimTrailingSlash(value) { return String(value ?? '').trim().replace(/\/+$/, ''); } function resolveRemoteAdapterEndpoint(endpoint) { const base = trimTrailingSlash(endpoint); return base || ''; } function normalizeOperationBasePath(value) { const raw = String(value ?? '').trim(); if (!raw) return '/mindspace/v1/adapter'; return `/${raw.replace(/^\/+/, '').replace(/\/+$/, '')}`; } function buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath = '/mindspace/v1/adapter') { const base = resolveRemoteAdapterEndpoint(endpoint); if (!base) { throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL'); } const normalizedBasePath = normalizeOperationBasePath(operationBasePath); return `${base}${normalizedBasePath}/${encodeURIComponent(bindingKey)}/${encodeURIComponent(method)}`; } function buildRemoteContractUrl(endpoint, contractPath = '/mindspace/v1/contract') { const base = resolveRemoteAdapterEndpoint(endpoint); if (!base) { throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL'); } const normalizedPath = normalizeOperationBasePath(contractPath); return `${base}${normalizedPath}`; } function throwRemoteOperationError(response, bindingKey, method, bodyText, payload) { const error = new Error( payload?.message ?? `MindSpace remote adapter ${bindingKey}.${method}() failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`, ); if (payload?.code) error.code = payload.code; if (payload?.details !== undefined) error.details = payload.details; throw error; } async function parseRemoteOperationResponse(response, bindingKey, method) { const bodyText = await response.text().catch(() => ''); let payload = null; if (bodyText) { try { payload = JSON.parse(bodyText); } catch { payload = null; } } if (!response.ok) { throwRemoteOperationError(response, bindingKey, method, bodyText, payload); } if (response.status === 204) return null; if (!bodyText) return null; return payload ?? JSON.parse(bodyText); } async function invokeRemoteOperation({ endpoint, operationBasePath, authToken, timeoutMs, bindingKey, method, args, fetchFn, }) { const controller = typeof AbortController === 'function' ? new AbortController() : null; const timeoutId = controller && Number.isFinite(timeoutMs) && timeoutMs > 0 ? setTimeout(() => controller.abort(new Error('MindSpace remote adapter request timed out')), timeoutMs) : null; try { const response = await fetchFn(buildRemoteOperationUrl(endpoint, bindingKey, method, operationBasePath), { method: 'POST', headers: { 'Content-Type': 'application/json', ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), }, body: JSON.stringify({ args, }), signal: controller?.signal, }); return parseRemoteOperationResponse(response, bindingKey, method); } finally { if (timeoutId) clearTimeout(timeoutId); } } function formatContractIssue(issue) { if (typeof issue === 'string') return issue; return JSON.stringify(issue); } export function validateMindSpaceRemoteContract( contract, { minContractVersion = MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION, requiredBindings = MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS, requiredCapabilities = MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES, } = {}, ) { const issues = []; const version = Number(contract?.contractVersion ?? 0); if (!Number.isFinite(version) || version < minContractVersion) { issues.push( `contractVersion ${contract?.contractVersion ?? '(missing)'} is below required ${minContractVersion}`, ); } const bindings = contract?.bindings ?? {}; for (const [bindingKey, methods] of Object.entries(requiredBindings ?? {})) { const exposed = bindings[bindingKey]; if (!Array.isArray(exposed)) { issues.push(`missing binding ${bindingKey}`); continue; } for (const method of methods) { if (!exposed.includes(method)) { issues.push(`missing method ${bindingKey}.${method}`); } } } const capabilities = new Set( Array.isArray(contract?.requiredCapabilities) ? contract.requiredCapabilities : [], ); for (const capability of requiredCapabilities ?? []) { if (!capabilities.has(capability)) { issues.push(`missing capability ${capability}`); } } if (issues.length > 0) { const error = new Error( `MindSpace remote contract is incompatible: ${issues .map(formatContractIssue) .join('; ')}`, ); error.code = 'mindspace_contract_incompatible'; error.details = { issues, contractVersion: contract?.contractVersion ?? null, buildId: contract?.buildId ?? null, gitSha: contract?.gitSha ?? null, }; throw error; } return contract; } async function fetchRemoteContract({ endpoint, contractPath, fetchFn, authToken, timeoutMs, }) { const controller = typeof AbortController === 'function' ? new AbortController() : null; const timeoutId = controller && Number.isFinite(timeoutMs) && timeoutMs > 0 ? setTimeout(() => controller.abort(new Error('MindSpace remote contract request timed out')), timeoutMs) : null; try { const response = await fetchFn(buildRemoteContractUrl(endpoint, contractPath), { method: 'GET', headers: { ...(authToken ? { Authorization: `Bearer ${authToken}` } : {}), }, signal: controller?.signal, }); const bodyText = await response.text().catch(() => ''); let payload = null; if (bodyText) { try { payload = JSON.parse(bodyText); } catch { payload = null; } } if (!response.ok) { const error = new Error( `MindSpace remote contract check failed with ${response.status}${bodyText ? `: ${bodyText}` : ''}`, ); error.code = 'mindspace_contract_unavailable'; throw error; } if (!payload) { const error = new Error('MindSpace remote contract response must be valid JSON'); error.code = 'mindspace_contract_invalid_json'; throw error; } return payload; } finally { if (timeoutId) clearTimeout(timeoutId); } } function createRemoteBindingProxy({ endpoint, bindingKey, fetchFn, authToken, timeoutMs, operationBasePath, }) { const methods = MINDSPACE_SERVER_ADAPTER_BINDINGS[bindingKey] ?? []; const methodSet = new Set(methods); const proxyTarget = {}; for (const method of methods) { proxyTarget[method] = async (...args) => invokeRemoteOperation({ endpoint, operationBasePath, authToken, timeoutMs, bindingKey, method, args, fetchFn, }); } return new Proxy(proxyTarget, { get(target, prop) { if (prop === Symbol.toStringTag) return 'MindSpaceRemoteBindingProxy'; if (typeof prop === 'string' && !methodSet.has(prop)) { throw new Error(`MindSpace remote adapter binding "${bindingKey}" does not expose ${bindingKey}.${prop}()`); } return target[prop]; }, }); } export function createMindSpaceRemoteServerAdapter({ logger = console, endpoint = '', authToken = '', operationBasePath = '/mindspace/v1/adapter', contractPath = '/mindspace/v1/contract', minContractVersion = MINDSPACE_SERVER_ADAPTER_CONTRACT_VERSION, requiredBindings = MINDSPACE_SERVER_ADAPTER_REQUIRED_BINDINGS, requiredCapabilities = MINDSPACE_SERVER_ADAPTER_REQUIRED_CAPABILITIES, timeoutMs = 15_000, fetchFn = globalThis.fetch?.bind(globalThis), } = {}) { const endpointUrl = resolveRemoteAdapterEndpoint(endpoint); const normalizedOperationBasePath = normalizeOperationBasePath(operationBasePath); const invoke = fetchFn ? fetchFn : async () => { throw new Error('MindSpace remote server adapter requires a fetch implementation'); }; const bindings = Object.fromEntries( MINDSPACE_SERVER_ADAPTER_BINDING_KEYS.map((bindingKey) => [ bindingKey, createRemoteBindingProxy({ endpoint: endpointUrl, bindingKey, fetchFn: invoke, authToken, timeoutMs, operationBasePath: normalizedOperationBasePath, }), ]), ); return { kind: 'remote', implementationStatus: 'scaffold', endpoint: endpointUrl, operationBasePath: normalizedOperationBasePath, contractPath: normalizeOperationBasePath(contractPath), timeoutMs, storageRoot: null, publicBaseUrl: null, storageAdapter: null, ...bindings, async assertReady() { if (!endpointUrl) { throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL'); } const contract = await fetchRemoteContract({ endpoint: endpointUrl, contractPath, fetchFn: invoke, authToken, timeoutMs, }); validateMindSpaceRemoteContract(contract, { minContractVersion, requiredBindings, requiredCapabilities, }); logger.log?.( `MindSpace remote contract ok (version=${contract.contractVersion}, build=${contract.buildId ?? 'unknown'}, git=${contract.gitSha ?? 'unknown'})`, ); return true; }, startBackgroundJobs() { logger.log?.( `MindSpace remote server adapter enabled${endpointUrl ? ` (${endpointUrl})` : ''}; background jobs stay on the remote runtime`, ); return { publicationCleanup: null, agentWorker: null, workspaceMaintenance: null, }; }, }; } export const mindspaceRemoteServerAdapterInternals = { buildRemoteContractUrl, buildRemoteOperationUrl, fetchRemoteContract, invokeRemoteOperation, normalizeOperationBasePath, validateMindSpaceRemoteContract, };