53eb0014ba
Return 404/401/403 from MindSpace RPC for known publication errors and preserve error codes through the Portal remote adapter. Co-authored-by: Cursor <cursoragent@cursor.com>
189 lines
5.4 KiB
JavaScript
189 lines
5.4 KiB
JavaScript
import {
|
|
MINDSPACE_SERVER_ADAPTER_BINDINGS,
|
|
MINDSPACE_SERVER_ADAPTER_BINDING_KEYS,
|
|
} 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 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 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',
|
|
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,
|
|
timeoutMs,
|
|
storageRoot: null,
|
|
publicBaseUrl: null,
|
|
storageAdapter: null,
|
|
...bindings,
|
|
assertReady() {
|
|
if (!endpointUrl) {
|
|
throw new Error('MindSpace remote server adapter requires MINDSPACE_REMOTE_BASE_URL');
|
|
}
|
|
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 = {
|
|
buildRemoteOperationUrl,
|
|
invokeRemoteOperation,
|
|
normalizeOperationBasePath,
|
|
};
|