From 1b9465b21aa2ba6d13c963be5a7a3af8e64a8d78 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 12:19:19 -0500 Subject: [PATCH 1/6] feat: add OpenAPI post-processing and improve index generation - Add post-processing script (scripts/update-openapi-index.ts) that runs after docusaurus gen-api-docs to rename .info.mdx files to index.mdx and regenerate the OpenAPI clients index with correct links - Update gen-api-docs-all script to chain post-processing automatically - Refactor preprocessing.ts: extract shared SERVICE_DESCRIPTIONS and CATEGORY_MAPPING constants, add catch-all category for uncategorized APIs - Add footer license info distinguishing Docs (CC BY 4.0) from Code (BSD 3-Clause) - Add gray-matter dependency for frontmatter parsing Co-Authored-By: Claude Sonnet 4.6 --- docusaurus.config.ts | 9 +- package.json | 5 +- scripts/update-openapi-index.ts | 11 +++ src/openapi/preprocessing.ts | 155 +++++++++++++++++++++++++++++--- 4 files changed, 165 insertions(+), 15 deletions(-) create mode 100644 scripts/update-openapi-index.ts diff --git a/docusaurus.config.ts b/docusaurus.config.ts index cb8d5e40..e5e0243e 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -198,8 +198,13 @@ const config: Config = { }, ], copyright: ` - Copyright Ā© ${new Date().getFullYear()} OpenTDF - +
+ Copyright Ā© ${new Date().getFullYear()} OpenTDF +
+
+ Documentation licensed under CC BY 4.0 • + Code licensed under BSD 3-Clause +
`, }, prism: { diff --git a/package.json b/package.json index 6ac4bdec..b73b0a8d 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,9 @@ "serve": "docusaurus serve", "write-translations": "docusaurus write-translations", "write-heading-ids": "docusaurus write-heading-ids", - "gen-api-docs-all": "docusaurus gen-api-docs all --all-versions", + "gen-api-docs-all": "docusaurus gen-api-docs all --all-versions && npm run update-openapi-index", "gen-api-docs-clean": "docusaurus clean-api-docs all", + "update-openapi-index": "tsx scripts/update-openapi-index.ts", "check-vendored-yaml": "tsx src/openapi/check-vendored-yaml.ts", "update-vendored-yaml": "tsx src/openapi/update-vendored-yaml.ts" }, @@ -34,7 +35,7 @@ "docusaurus-theme-github-codeblock": "^2.0.2", "docusaurus-theme-openapi-docs": "^4.4.0", "gray-matter": "^4.0.3", - "minimatch": "^10.2.1", + "minimatch": "^10.0.1", "octokit": "^4.0.2", "plugin-image-zoom": "^1.2.0", "prism-react-renderer": "^2.4.0", diff --git a/scripts/update-openapi-index.ts b/scripts/update-openapi-index.ts new file mode 100644 index 00000000..9c8dd33a --- /dev/null +++ b/scripts/update-openapi-index.ts @@ -0,0 +1,11 @@ +/** + * Post-processing script to update the OpenAPI index page with correct links + * after the OpenAPI docs have been generated. + */ + +import { updateOpenApiIndex, renameInfoFilesToIndex } from '../src/openapi/preprocessing'; + +console.log('šŸ”„ Running post-generation OpenAPI processing...'); +renameInfoFilesToIndex(); +updateOpenApiIndex(); +console.log('āœ… OpenAPI post-processing complete'); diff --git a/src/openapi/preprocessing.ts b/src/openapi/preprocessing.ts index 1e410980..d886855f 100644 --- a/src/openapi/preprocessing.ts +++ b/src/openapi/preprocessing.ts @@ -4,6 +4,7 @@ When making changes to this file, consider: https://virtru.atlassian.net/browse/ import * as fs from 'fs'; import * as path from 'path'; import * as yaml from 'js-yaml'; +import matter from 'gray-matter'; import type * as OpenApiPlugin from "docusaurus-plugin-openapi-docs"; // Utility to find the repo root (directory containing package.json) @@ -30,6 +31,35 @@ const OUTPUT_PREFIX = path.join(repoRoot, 'docs', 'OpenAPI-clients'); // The index page for OpenAPI documentation, to support bookmarking & sharing the URL const OPENAPI_INDEX_PAGE = `${OUTPUT_PREFIX}/index.md`; +// Service descriptions and categorization for OpenAPI index generation +const SERVICE_DESCRIPTIONS: Record = { + 'Well-Known Configuration': 'Platform configuration and service discovery', + 'kas': 'Key Access Service for TDF encryption/decryption', + 'V1 Authorization': 'Authorization decisions (v1)', + 'V2 Authorization': 'Authorization decisions (v2)', + 'V1 Entity Resolution': 'Entity resolution from JWT tokens (v1)', + 'V2 Entity Resolution': 'Entity resolution from tokens (v2)', + 'Policy Objects': 'Core policy objects and management', + 'Policy Attributes': 'Attribute definitions and values', + 'Policy Namespaces': 'Namespace management', + 'Policy Actions': 'Action definitions', + 'Policy Subject Mapping': 'Map subjects to attributes', + 'Policy Resource Mapping': 'Map resources to attributes', + 'Policy Obligations': 'Usage obligations and triggers', + 'Policy Registered Resources': 'Resource registration', + 'Policy KAS Registry': 'KAS registration and management', + 'Key Management': 'Cryptographic key management', + 'Policy Unsafe Service': 'Administrative operations', +}; + +const CATEGORY_MAPPING: Record = { + 'Core Services': ['Well-Known Configuration', 'kas'], + 'Authorization & Entity Resolution': ['V1 Authorization', 'V2 Authorization', 'V1 Entity Resolution', 'V2 Entity Resolution'], + 'Policy Management': ['Policy Objects', 'Policy Attributes', 'Policy Namespaces', 'Policy Actions', + 'Policy Subject Mapping', 'Policy Resource Mapping', 'Policy Obligations', + 'Policy Registered Resources', 'Policy KAS Registry', 'Key Management', 'Policy Unsafe Service'], +}; + // Read BUILD_OPENAPI_SAMPLES once const BUILD_OPENAPI_SAMPLES = process.env.BUILD_OPENAPI_SAMPLES === '1'; @@ -414,27 +444,130 @@ async function preprocessOpenApiSpecs() { delete spec.specPathModified; } - // Create the index page for OpenAPI documentation + console.log('✨ OpenAPI preprocessing complete'); +}; + + +/** + * Renames all .info.mdx files to index.mdx so they become category index pages + * instead of appearing as separate items in the sidebar. + */ +function renameInfoFilesToIndex() { + console.log('šŸ”„ Renaming .info.mdx files to index.mdx...'); + + function processDirectory(dir: string) { + if (!fs.existsSync(dir)) return; + + const items = fs.readdirSync(dir, { withFileTypes: true }); + + for (const item of items) { + const fullPath = path.join(dir, item.name); + + if (item.isDirectory()) { + processDirectory(fullPath); + } else if (item.name.endsWith('.info.mdx')) { + const newPath = path.join(dir, 'index.mdx'); + fs.renameSync(fullPath, newPath); + console.log(` Renamed: ${fullPath} → ${newPath}`); + } + } + } + + processDirectory(OUTPUT_PREFIX); + console.log('āœ… Renamed all .info.mdx files to index.mdx'); +} + +/** + * Updates the OpenAPI index page with links to generated docs. + * This should be called AFTER the OpenAPI docs have been generated. + */ +function updateOpenApiIndex() { + console.log('šŸ“ Updating OpenAPI index page with generated doc links...'); + + // Helper function to find and read the document ID from a generated index.mdx file + function getDocIdFromInfoFile(outputDir: string): string | null { + try { + if (!fs.existsSync(outputDir)) { + return null; + } + + const indexPath = path.join(outputDir, 'index.mdx'); + + if (fs.existsSync(indexPath)) { + const fileContent = fs.readFileSync(indexPath, 'utf8'); + const parsed = matter(fileContent); + + if (parsed.data.id) { + const relativePath = path.relative(OUTPUT_PREFIX, outputDir); + return `OpenAPI-clients/${relativePath}`; + } + } + } catch (error) { + // Silently skip if file doesn't exist yet + } + return null; + } + + // Build service links dynamically from openApiSpecsArray + // Track which specs are categorized to find uncategorized ones + const categorizedSpecs = new Set(); + let serviceLinksMarkdown = ''; + + Object.entries(CATEGORY_MAPPING).forEach(([category, specIds]) => { + serviceLinksMarkdown += `\n## ${category}\n\n`; + specIds.forEach(specId => { + categorizedSpecs.add(specId); + const spec = openApiSpecsArray.find(s => s.id === specId); + if (spec) { + const docId = getDocIdFromInfoFile(spec.outputDir); + if (docId) { + const description = SERVICE_DESCRIPTIONS[specId] || 'API documentation'; + serviceLinksMarkdown += `- **[${spec.id}](/${docId})** - ${description}\n`; + } + } + }); + }); + + // Add uncategorized APIs to a catch-all category + const uncategorizedSpecs = openApiSpecsArray.filter(spec => !categorizedSpecs.has(spec.id)); + if (uncategorizedSpecs.length > 0) { + console.warn(`āš ļø Found ${uncategorizedSpecs.length} uncategorized API(s): ${uncategorizedSpecs.map(s => s.id).join(', ')}`); + serviceLinksMarkdown += `\n## Other APIs\n\n`; + uncategorizedSpecs.forEach(spec => { + const docId = getDocIdFromInfoFile(spec.outputDir); + if (docId) { + const description = SERVICE_DESCRIPTIONS[spec.id] || 'API documentation'; + serviceLinksMarkdown += `- **[${spec.id}](/${docId})** - ${description}\n`; + } + }); + } + const indexContent = `--- title: OpenAPI Clients sidebar_position: 7 --- # OpenAPI Clients -OpenAPI client examples are available for platform endpoints. +Interactive API documentation for OpenTDF Platform services. Each endpoint includes request/response examples, parameter descriptions, and the ability to try requests directly in your browser. -Expand each section in the navigation panel to access the OpenAPI documentation for each service. -` +${serviceLinksMarkdown} - // Ensure the file 'OPENAPI_INDEX_PAGE' exists - fs.mkdirSync(path.dirname(OPENAPI_INDEX_PAGE), { recursive: true }); +## Getting Started - fs.writeFileSync(OPENAPI_INDEX_PAGE, indexContent, 'utf8'); - console.log(`āœ… Created OpenAPI index page at ${OPENAPI_INDEX_PAGE}`); +1. Select a service from the list above or navigation sidebar +2. Browse available endpoints and operations +3. Review request parameters and response schemas +4. Test endpoints using the "Try it" feature - console.log('✨ OpenAPI preprocessing complete'); -}; +## Authentication + +Most endpoints require authentication. Configure your access token in the API documentation interface before testing endpoints. +`; + fs.mkdirSync(path.dirname(OPENAPI_INDEX_PAGE), { recursive: true }); + fs.writeFileSync(OPENAPI_INDEX_PAGE, indexContent, 'utf8'); + console.log(`āœ… Updated OpenAPI index page at ${OPENAPI_INDEX_PAGE}`); +} // Export the function and data without automatically executing it -export { openApiSpecs, openApiSpecsArray, preprocessOpenApiSpecs }; \ No newline at end of file +export { openApiSpecs, openApiSpecsArray, preprocessOpenApiSpecs, updateOpenApiIndex, renameInfoFilesToIndex }; \ No newline at end of file From da67f4c584766bd0617795bf256c6c681aa94d38 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 17:45:15 -0500 Subject: [PATCH 2/6] fix: restore minimatch to ^10.2.1 to match main Co-Authored-By: Claude Sonnet 4.6 --- package-lock.json | 22 ++++++++++++++++++++++ package.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 72df6c82..40cdc6ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4565,6 +4565,7 @@ "version": "1.4.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -5093,6 +5094,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5115,6 +5117,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5137,6 +5140,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5153,6 +5157,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5169,6 +5174,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5185,6 +5191,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5201,6 +5208,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5217,6 +5225,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5233,6 +5242,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5249,6 +5259,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5265,6 +5276,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -5281,6 +5293,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5303,6 +5316,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5325,6 +5339,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5347,6 +5362,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5369,6 +5385,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5391,6 +5408,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -5413,6 +5431,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -5432,6 +5451,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -5451,6 +5471,7 @@ "cpu": [ "ia32" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -5470,6 +5491,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ diff --git a/package.json b/package.json index b73b0a8d..23382a91 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "docusaurus-theme-github-codeblock": "^2.0.2", "docusaurus-theme-openapi-docs": "^4.4.0", "gray-matter": "^4.0.3", - "minimatch": "^10.0.1", + "minimatch": "^10.2.1", "octokit": "^4.0.2", "plugin-image-zoom": "^1.2.0", "prism-react-renderer": "^2.4.0", From faebab4508c26fb76f6c7476ab13ba338f356127 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 18:08:17 -0500 Subject: [PATCH 3/6] fix: address Gemini code review feedback - Move footer license inline styles to CSS class (footer__license-info) - Log unexpected errors in getDocIdFromInfoFile catch block instead of swallowing them silently - Use Map for O(1) spec lookups instead of Array.find() inside loop Co-Authored-By: Claude Sonnet 4.6 --- docusaurus.config.ts | 6 +++--- src/css/custom.css | 13 +++++++++++++ src/openapi/preprocessing.ts | 9 +++++++-- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docusaurus.config.ts b/docusaurus.config.ts index e5e0243e..0f4d5478 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -201,9 +201,9 @@ const config: Config = {
Copyright Ā© ${new Date().getFullYear()} OpenTDF
-
- Documentation licensed under CC BY 4.0 • - Code licensed under BSD 3-Clause + `, }, diff --git a/src/css/custom.css b/src/css/custom.css index 4054e8a9..6ed14ec6 100644 --- a/src/css/custom.css +++ b/src/css/custom.css @@ -371,3 +371,16 @@ div[class*="language-shell"] code::before { html[data-theme='dark'] div[class*="language-shell"] code::before { color: var(--ifm-color-gray-400); } + +/************** +** FOOTER +***************/ +.footer__license-info { + margin-top: 0.5rem; + font-size: 0.85rem; + opacity: 0.8; +} + +.footer__license-info a { + text-decoration: underline; +} diff --git a/src/openapi/preprocessing.ts b/src/openapi/preprocessing.ts index d886855f..0126208e 100644 --- a/src/openapi/preprocessing.ts +++ b/src/openapi/preprocessing.ts @@ -503,13 +503,18 @@ function updateOpenApiIndex() { } } } catch (error) { - // Silently skip if file doesn't exist yet + if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') { + // File doesn't exist yet — expected during preprocessing phase + } else { + console.warn(`Could not read or parse info file in ${outputDir}:`, error); + } } return null; } // Build service links dynamically from openApiSpecsArray // Track which specs are categorized to find uncategorized ones + const specsById = new Map(openApiSpecsArray.map(spec => [spec.id, spec])); const categorizedSpecs = new Set(); let serviceLinksMarkdown = ''; @@ -517,7 +522,7 @@ function updateOpenApiIndex() { serviceLinksMarkdown += `\n## ${category}\n\n`; specIds.forEach(specId => { categorizedSpecs.add(specId); - const spec = openApiSpecsArray.find(s => s.id === specId); + const spec = specsById.get(specId); if (spec) { const docId = getDocIdFromInfoFile(spec.outputDir); if (docId) { From 862bc08e44a05cc096685cf0049a4994d580295e Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 20:53:30 -0500 Subject: [PATCH 4/6] refactor: apply best-practice fixes to OpenAPI preprocessing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add try/catch + process.exit(1) to update-openapi-index.ts script - Use path.join for OPENAPI_INDEX_PAGE construction - Annotate samplesConfiguration with explicit Record type - Hoist getDocIdFromInfoFile to module level (was nested inside updateOpenApiIndex) - Remove redundant fs.existsSync guards (TOCTOU pattern) — rely on try/catch ENOENT - Replace delete spec.specPathModified mutation with clean destructuring to rebuild entry Co-Authored-By: Claude Sonnet 4.6 --- scripts/update-openapi-index.ts | 13 ++++--- src/openapi/preprocessing.ts | 62 +++++++++++++++------------------ 2 files changed, 37 insertions(+), 38 deletions(-) diff --git a/scripts/update-openapi-index.ts b/scripts/update-openapi-index.ts index 9c8dd33a..23276c64 100644 --- a/scripts/update-openapi-index.ts +++ b/scripts/update-openapi-index.ts @@ -5,7 +5,12 @@ import { updateOpenApiIndex, renameInfoFilesToIndex } from '../src/openapi/preprocessing'; -console.log('šŸ”„ Running post-generation OpenAPI processing...'); -renameInfoFilesToIndex(); -updateOpenApiIndex(); -console.log('āœ… OpenAPI post-processing complete'); +try { + console.log('šŸ”„ Running post-generation OpenAPI processing...'); + renameInfoFilesToIndex(); + updateOpenApiIndex(); + console.log('āœ… OpenAPI post-processing complete'); +} catch (error) { + console.error('āŒ OpenAPI post-processing failed:', error); + process.exit(1); +} diff --git a/src/openapi/preprocessing.ts b/src/openapi/preprocessing.ts index 0126208e..4d8c85a7 100644 --- a/src/openapi/preprocessing.ts +++ b/src/openapi/preprocessing.ts @@ -29,7 +29,7 @@ const ADD_TIMESTAMP_TO_DESCRIPTION = false; const OUTPUT_PREFIX = path.join(repoRoot, 'docs', 'OpenAPI-clients'); // The index page for OpenAPI documentation, to support bookmarking & sharing the URL -const OPENAPI_INDEX_PAGE = `${OUTPUT_PREFIX}/index.md`; +const OPENAPI_INDEX_PAGE = path.join(OUTPUT_PREFIX, 'index.md'); // Service descriptions and categorization for OpenAPI index generation const SERVICE_DESCRIPTIONS: Record = { @@ -64,7 +64,7 @@ const CATEGORY_MAPPING: Record = { const BUILD_OPENAPI_SAMPLES = process.env.BUILD_OPENAPI_SAMPLES === '1'; // Initialize empty samples configuration - will be populated conditionally -let samplesConfiguration = {}; +let samplesConfiguration: Record = {}; interface ApiSpecDefinition { id: string; // Unique key for the API spec, e.g., "authorization" @@ -438,10 +438,9 @@ async function preprocessOpenApiSpecs() { console.error(`āŒ Error processing ${sourcePath}:`, error); } - spec.specPath = spec.specPathModified; // Update the original specPath to the modified one - - // Delete the specPathModified property to avoid confusion - delete spec.specPathModified; + // Rebuild the entry with the processed path, dropping specPathModified cleanly + const { specPathModified: _, ...cleanSpec } = spec; + openApiSpecs[id] = { ...cleanSpec, specPath: spec.specPathModified! }; } console.log('✨ OpenAPI preprocessing complete'); @@ -477,6 +476,29 @@ function renameInfoFilesToIndex() { console.log('āœ… Renamed all .info.mdx files to index.mdx'); } +/** + * Reads the document ID from a generated index.mdx file and returns the doc path, + * or null if the file does not exist or has no id in frontmatter. + */ +function getDocIdFromInfoFile(outputDir: string): string | null { + try { + const indexPath = path.join(outputDir, 'index.mdx'); + const fileContent = fs.readFileSync(indexPath, 'utf8'); + const parsed = matter(fileContent); + if (parsed.data.id) { + const relativePath = path.relative(OUTPUT_PREFIX, outputDir); + return `OpenAPI-clients/${relativePath}`; + } + } catch (error) { + if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') { + // File doesn't exist yet — expected during preprocessing phase + } else { + console.warn(`Could not read or parse info file in ${outputDir}:`, error); + } + } + return null; +} + /** * Updates the OpenAPI index page with links to generated docs. * This should be called AFTER the OpenAPI docs have been generated. @@ -484,34 +506,6 @@ function renameInfoFilesToIndex() { function updateOpenApiIndex() { console.log('šŸ“ Updating OpenAPI index page with generated doc links...'); - // Helper function to find and read the document ID from a generated index.mdx file - function getDocIdFromInfoFile(outputDir: string): string | null { - try { - if (!fs.existsSync(outputDir)) { - return null; - } - - const indexPath = path.join(outputDir, 'index.mdx'); - - if (fs.existsSync(indexPath)) { - const fileContent = fs.readFileSync(indexPath, 'utf8'); - const parsed = matter(fileContent); - - if (parsed.data.id) { - const relativePath = path.relative(OUTPUT_PREFIX, outputDir); - return `OpenAPI-clients/${relativePath}`; - } - } - } catch (error) { - if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') { - // File doesn't exist yet — expected during preprocessing phase - } else { - console.warn(`Could not read or parse info file in ${outputDir}:`, error); - } - } - return null; - } - // Build service links dynamically from openApiSpecsArray // Track which specs are categorized to find uncategorized ones const specsById = new Map(openApiSpecsArray.map(spec => [spec.id, spec])); From f30c90ad6b194927bb083b6c8f9a1f2ddf19f68f Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 21:24:39 -0500 Subject: [PATCH 5/6] fix: address second round of Gemini code review feedback - Guard fs.renameSync with existsSync check to avoid cross-OS inconsistency when destination index.mdx already exists - Log warning when ENOENT in getDocIdFromInfoFile (post-processing context means missing file likely indicates a doc generation failure) - Warn when SERVICE_DESCRIPTIONS entry is missing instead of silently falling back to generic "API documentation" string Co-Authored-By: Claude Sonnet 4.6 --- src/openapi/preprocessing.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/openapi/preprocessing.ts b/src/openapi/preprocessing.ts index 4d8c85a7..02fd8f20 100644 --- a/src/openapi/preprocessing.ts +++ b/src/openapi/preprocessing.ts @@ -466,8 +466,12 @@ function renameInfoFilesToIndex() { processDirectory(fullPath); } else if (item.name.endsWith('.info.mdx')) { const newPath = path.join(dir, 'index.mdx'); - fs.renameSync(fullPath, newPath); - console.log(` Renamed: ${fullPath} → ${newPath}`); + if (fs.existsSync(newPath)) { + console.warn(`āš ļø Skipping rename of ${fullPath} because destination ${newPath} already exists.`); + } else { + fs.renameSync(fullPath, newPath); + console.log(` Renamed: ${fullPath} → ${newPath}`); + } } } } @@ -491,7 +495,8 @@ function getDocIdFromInfoFile(outputDir: string): string | null { } } catch (error) { if (error instanceof Error && 'code' in error && (error as NodeJS.ErrnoException).code === 'ENOENT') { - // File doesn't exist yet — expected during preprocessing phase + // Missing index file likely means doc generation failed for this spec — link will be skipped. + console.warn(`āš ļø Could not find index file for an API spec, so it will be skipped. Path: ${path.join(outputDir, 'index.mdx')}`); } else { console.warn(`Could not read or parse info file in ${outputDir}:`, error); } @@ -520,7 +525,11 @@ function updateOpenApiIndex() { if (spec) { const docId = getDocIdFromInfoFile(spec.outputDir); if (docId) { - const description = SERVICE_DESCRIPTIONS[specId] || 'API documentation'; + let description = SERVICE_DESCRIPTIONS[specId]; + if (!description) { + console.warn(`āš ļø Missing description for service "${specId}". Using default.`); + description = 'API documentation'; + } serviceLinksMarkdown += `- **[${spec.id}](/${docId})** - ${description}\n`; } } @@ -535,7 +544,11 @@ function updateOpenApiIndex() { uncategorizedSpecs.forEach(spec => { const docId = getDocIdFromInfoFile(spec.outputDir); if (docId) { - const description = SERVICE_DESCRIPTIONS[spec.id] || 'API documentation'; + let description = SERVICE_DESCRIPTIONS[spec.id]; + if (!description) { + console.warn(`āš ļø Missing description for service "${spec.id}". Using default.`); + description = 'API documentation'; + } serviceLinksMarkdown += `- **[${spec.id}](/${docId})** - ${description}\n`; } }); From 8b9a0c245e3ec75949a106338139363d87db05b4 Mon Sep 17 00:00:00 2001 From: jp-ayyappan Date: Sat, 21 Feb 2026 22:42:58 -0500 Subject: [PATCH 6/6] feat: detect unregistered spec files in platform repo Extend check-vendored-yaml to also walk docs/openapi/ in the platform repo via the GitHub Contents API and flag any .yaml files that are not registered in openApiSpecsArray. Specs with no paths (shared schema files like common and entity) are skipped automatically. If a new API spec is added to the platform repo without a corresponding entry in preprocessing.ts, the build will now fail with a clear message pointing to the missing registration. Co-Authored-By: Claude Sonnet 4.6 --- src/openapi/check-vendored-yaml.ts | 92 ++++++++++++++++++++++++++++-- 1 file changed, 88 insertions(+), 4 deletions(-) diff --git a/src/openapi/check-vendored-yaml.ts b/src/openapi/check-vendored-yaml.ts index f82bd05d..285f21fd 100644 --- a/src/openapi/check-vendored-yaml.ts +++ b/src/openapi/check-vendored-yaml.ts @@ -3,8 +3,12 @@ When making changes to this file, consider: https://virtru.atlassian.net/browse/ */ import * as fs from 'fs'; import * as crypto from 'crypto'; +import * as yaml from 'js-yaml'; import { openApiSpecsArray } from './preprocessing'; +const PLATFORM_API_BASE = 'https://api.github.com/repos/opentdf/platform'; +const PLATFORM_RAW_BASE = 'https://raw.githubusercontent.com/opentdf/platform/refs/heads/main'; + function fileHash(filePath: string): string { if (!fs.existsSync(filePath)) return ''; const data = fs.readFileSync(filePath); @@ -32,16 +36,70 @@ function downloadFile(url: string, dest: string): Promise { }); } +function fetchJson(url: string): Promise { + return new Promise((resolve, reject) => { + import('https').then(https => { + https.get(url, { headers: { 'User-Agent': 'opentdf-docs-check-vendored-yaml' } } as any, (response: any) => { + let data = ''; + response.on('data', (chunk: string) => { data += chunk; }); + response.on('end', () => { + try { resolve(JSON.parse(data)); } + catch (e) { reject(new Error(`Failed to parse JSON from ${url}: ${e}`)); } + }); + }).on('error', reject); + }).catch(reject); + }); +} + +function fetchText(url: string): Promise { + return new Promise((resolve, reject) => { + import('https').then(https => { + https.get(url, { headers: { 'User-Agent': 'opentdf-docs-check-vendored-yaml' } } as any, (response: any) => { + let data = ''; + response.on('data', (chunk: string) => { data += chunk; }); + response.on('end', () => resolve(data)); + }).on('error', reject); + }).catch(reject); + }); +} + +/** + * Recursively fetches all .yaml file paths under docs/openapi/ in the platform repo. + */ +async function fetchRemoteSpecPaths(dirPath = 'docs/openapi'): Promise { + const specPaths: string[] = []; + const contents = await fetchJson(`${PLATFORM_API_BASE}/contents/${dirPath}`); + + for (const item of contents) { + if (item.type === 'file' && item.name.endsWith('.yaml')) { + specPaths.push(item.path); + } else if (item.type === 'dir') { + specPaths.push(...await fetchRemoteSpecPaths(item.path)); + } + } + + return specPaths; +} + +/** + * Returns true if the spec at the given raw URL has actual API paths defined + * (i.e. it is a real API spec, not a shared schema-only file like common or entity). + */ +async function hasApiPaths(rawUrl: string): Promise { + const content = await fetchText(rawUrl); + const spec = yaml.load(content) as any; + return spec?.paths != null && Object.keys(spec.paths).length > 0; +} + async function main() { let hasDiff = false; + + // --- Check 1: vendored files are up to date --- for (const spec of openApiSpecsArray) { - if (!spec.url) continue; // Only process specs with a URL - // absPaths is the absolute path to the spec file + if (!spec.url) continue; const absPath = spec.specPath; const tmpPath = absPath + '.tmp'; - // Download to tmpPath await downloadFile(spec.url, tmpPath); - // Compare hashes const oldHash = fileHash(absPath); const newHash = fileHash(tmpPath); if (oldHash !== newHash) { @@ -52,6 +110,32 @@ async function main() { } fs.unlinkSync(tmpPath); } + + // --- Check 2: no unregistered spec files in the platform repo --- + console.log('\nšŸ” Checking for unregistered spec files in opentdf/platform...'); + const registeredUrls = new Set( + openApiSpecsArray.flatMap(spec => spec.url ? [spec.url] : []) + ); + + const remoteSpecPaths = await fetchRemoteSpecPaths(); + + for (const remotePath of remoteSpecPaths) { + const expectedUrl = `${PLATFORM_RAW_BASE}/${remotePath}`; + if (registeredUrls.has(expectedUrl)) continue; + + // Not registered — check if it actually has API paths (vs shared schema file) + if (await hasApiPaths(expectedUrl)) { + hasDiff = true; + console.error( + `āŒ Unregistered spec found in platform repo: ${remotePath}\n` + + ` Add an entry to openApiSpecsArray in src/openapi/preprocessing.ts with:\n` + + ` url: '${expectedUrl}'` + ); + } else { + console.log(`ā„¹ļø Skipping schema-only file (no paths): ${remotePath}`); + } + } + process.exit(hasDiff ? 1 : 0); }