chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Fetches and filters the OpenAPI specs for fumadocs.
|
||||
* Outputs two separate spec files:
|
||||
* - public/openapi.json (v3.1 — latest, clean operationIds)
|
||||
* - public/openapi-v3.json (v3.0)
|
||||
*
|
||||
* Run: bun run scripts/fetch-openapi.mjs
|
||||
*/
|
||||
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, join } from 'node:path';
|
||||
|
||||
const OPENAPI_V3_URL = process.env.OPENAPI_SPEC_URL || 'https://backend.composio.dev/api/v3/openapi.json';
|
||||
const OPENAPI_V31_URL = process.env.OPENAPI_V31_SPEC_URL || 'https://backend.composio.dev/api/v3.1/openapi.json';
|
||||
|
||||
// Tags to ignore (internal/admin)
|
||||
const IGNORED_TAGS = [
|
||||
'CLI',
|
||||
'Admin',
|
||||
'Profiling',
|
||||
'User',
|
||||
'x-internal',
|
||||
];
|
||||
|
||||
async function fetchSpec(url) {
|
||||
console.log(`Fetching OpenAPI spec from ${url}...`);
|
||||
const response = await fetch(url);
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter paths: remove ignored/internal tags, keep first tag only.
|
||||
*/
|
||||
function filterPaths(paths) {
|
||||
const filteredPaths = {};
|
||||
let removedCount = 0;
|
||||
|
||||
for (const [path, methods] of Object.entries(paths)) {
|
||||
const filteredMethods = {};
|
||||
|
||||
for (const [method, operation] of Object.entries(methods)) {
|
||||
const tags = operation.tags || [];
|
||||
const hasValidTag = tags.some(tag => !IGNORED_TAGS.includes(tag));
|
||||
|
||||
if (!hasValidTag && tags.length > 0) {
|
||||
removedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (operation['x-internal'] === true || tags.includes('x-internal')) {
|
||||
removedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (tags.length > 1) {
|
||||
operation.tags = [tags[0]];
|
||||
}
|
||||
|
||||
filteredMethods[method] = operation;
|
||||
}
|
||||
|
||||
if (Object.keys(filteredMethods).length > 0) {
|
||||
filteredPaths[path] = filteredMethods;
|
||||
}
|
||||
}
|
||||
|
||||
return { filteredPaths, removedCount };
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip version prefixes from operationIds (e.g. getV3_1Tools → getTools).
|
||||
*/
|
||||
function cleanOperationIds(paths) {
|
||||
for (const methods of Object.values(paths)) {
|
||||
for (const operation of Object.values(methods)) {
|
||||
if (operation.operationId) {
|
||||
// Remove V3_1, V3_0, etc. prefixes from operationId
|
||||
operation.operationId = operation.operationId.replace(/V\d+_\d+/g, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Post-process a spec: remove CookieAuth, normalize unions, fix nullable.
|
||||
*/
|
||||
function postProcessSpec(spec) {
|
||||
// Pin the server to production. The published docs must always show the
|
||||
// production base URL in their curl examples, regardless of which environment
|
||||
// the source spec was fetched from (a staging fetch would otherwise bake a
|
||||
// staging server URL into the committed reference).
|
||||
spec.servers = [
|
||||
{
|
||||
url: 'https://backend.composio.dev',
|
||||
description: 'PRODUCTION API',
|
||||
},
|
||||
];
|
||||
|
||||
// Filter tags list
|
||||
if (spec.tags) {
|
||||
spec.tags = spec.tags.filter(tag => !IGNORED_TAGS.includes(tag.name));
|
||||
}
|
||||
|
||||
// Remove CookieAuth from security schemes
|
||||
if (spec.components?.securitySchemes?.CookieAuth) {
|
||||
delete spec.components.securitySchemes.CookieAuth;
|
||||
}
|
||||
|
||||
// Remove CookieAuth from all endpoint security arrays
|
||||
for (const methods of Object.values(spec.paths)) {
|
||||
for (const operation of Object.values(methods)) {
|
||||
if (operation.security) {
|
||||
operation.security = operation.security.filter(sec => !('CookieAuth' in sec));
|
||||
if (operation.security.length === 0) {
|
||||
delete operation.security;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize overly complex anyOf/oneOf schemas
|
||||
const mergePropertySchemas = (existing, incoming) => {
|
||||
if (!existing) return JSON.parse(JSON.stringify(incoming));
|
||||
const merged = JSON.parse(JSON.stringify(existing));
|
||||
if (existing.enum && incoming.enum) {
|
||||
merged.enum = [...new Set([...existing.enum, ...incoming.enum])];
|
||||
}
|
||||
if (existing.properties && incoming.properties) {
|
||||
merged.properties = { ...existing.properties };
|
||||
for (const [key, val] of Object.entries(incoming.properties)) {
|
||||
merged.properties[key] = mergePropertySchemas(merged.properties[key], val);
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
};
|
||||
|
||||
const normalizeUnionSchemas = (obj) => {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
for (const unionKey of ['anyOf', 'oneOf']) {
|
||||
if (obj[unionKey] && Array.isArray(obj[unionKey]) && obj[unionKey].length > 5) {
|
||||
const objectSchemas = obj[unionKey].filter(s => s.type === 'object' && s.properties);
|
||||
if (objectSchemas.length > 5 && objectSchemas.length >= obj[unionKey].length * 0.8) {
|
||||
const mergedProperties = {};
|
||||
const allRequired = new Set();
|
||||
for (const schema of objectSchemas) {
|
||||
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
||||
mergedProperties[propName] = mergePropertySchemas(mergedProperties[propName], propSchema);
|
||||
}
|
||||
if (schema.required) {
|
||||
for (const req of schema.required) allRequired.add(req);
|
||||
}
|
||||
}
|
||||
const universallyRequired = [...allRequired].filter(req =>
|
||||
objectSchemas.every(s => s.required && s.required.includes(req))
|
||||
);
|
||||
delete obj[unionKey];
|
||||
obj.type = 'object';
|
||||
obj.properties = mergedProperties;
|
||||
if (universallyRequired.length > 0) obj.required = universallyRequired;
|
||||
obj.additionalProperties = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const val of Object.values(obj)) {
|
||||
if (Array.isArray(val)) val.forEach(item => normalizeUnionSchemas(item));
|
||||
else normalizeUnionSchemas(val);
|
||||
}
|
||||
};
|
||||
normalizeUnionSchemas(spec);
|
||||
|
||||
// Fix invalid OpenAPI 3.0: "nullable: true" without "type"
|
||||
const fixNullableWithoutType = (obj, parentKey = '') => {
|
||||
if (!obj || typeof obj !== 'object') return;
|
||||
if (obj.nullable === true && !obj.type && !obj.$ref && !obj.oneOf && !obj.anyOf && !obj.allOf) {
|
||||
if (parentKey === 'additionalProperties') {
|
||||
delete obj.nullable;
|
||||
} else if (obj.example && typeof obj.example === 'object' && !Array.isArray(obj.example)) {
|
||||
obj.type = 'object';
|
||||
} else if (obj.example && Array.isArray(obj.example)) {
|
||||
obj.type = 'array';
|
||||
} else {
|
||||
obj.type = 'object';
|
||||
}
|
||||
}
|
||||
for (const [key, val] of Object.entries(obj)) {
|
||||
if (Array.isArray(val)) val.forEach(item => fixNullableWithoutType(item, key));
|
||||
else fixNullableWithoutType(val, key);
|
||||
}
|
||||
};
|
||||
fixNullableWithoutType(spec);
|
||||
}
|
||||
|
||||
async function fetchAndFilterSpec() {
|
||||
// Fetch both specs in parallel
|
||||
const [v3Raw, v31Raw] = await Promise.all([
|
||||
fetchSpec(OPENAPI_V3_URL),
|
||||
fetchSpec(OPENAPI_V31_URL),
|
||||
]);
|
||||
|
||||
// --- v3.1 spec (latest, default) ---
|
||||
const v31Spec = JSON.parse(JSON.stringify(v31Raw));
|
||||
const v31Filtered = filterPaths(v31Spec.paths);
|
||||
v31Spec.paths = v31Filtered.filteredPaths;
|
||||
// Clean operationIds: getV3_1Tools → getTools (so URLs are clean)
|
||||
cleanOperationIds(v31Spec.paths);
|
||||
// Annotate all operations with version
|
||||
for (const methods of Object.values(v31Spec.paths)) {
|
||||
for (const op of Object.values(methods)) {
|
||||
op['x-api-version'] = '3.1';
|
||||
}
|
||||
}
|
||||
postProcessSpec(v31Spec);
|
||||
console.log(`v3.1: ${Object.keys(v31Spec.paths).length} paths`);
|
||||
|
||||
// --- v3.0 spec ---
|
||||
const v3Spec = JSON.parse(JSON.stringify(v3Raw));
|
||||
const v3Filtered = filterPaths(v3Spec.paths);
|
||||
v3Spec.paths = v3Filtered.filteredPaths;
|
||||
cleanOperationIds(v3Spec.paths);
|
||||
for (const methods of Object.values(v3Spec.paths)) {
|
||||
for (const op of Object.values(methods)) {
|
||||
op['x-api-version'] = '3.0';
|
||||
}
|
||||
}
|
||||
postProcessSpec(v3Spec);
|
||||
console.log(`v3.0: ${Object.keys(v3Spec.paths).length} paths`);
|
||||
|
||||
// Write both spec files
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
const v31Path = join(__dirname, '../public/openapi.json');
|
||||
writeFileSync(v31Path, JSON.stringify(v31Spec, null, 2));
|
||||
console.log(`Written v3.1 spec to ${v31Path}`);
|
||||
|
||||
const v3Path = join(__dirname, '../public/openapi-v3.json');
|
||||
writeFileSync(v3Path, JSON.stringify(v3Spec, null, 2));
|
||||
console.log(`Written v3.0 spec to ${v3Path}`);
|
||||
}
|
||||
|
||||
fetchAndFilterSpec().catch(console.error);
|
||||
Reference in New Issue
Block a user