diff --git a/docusaurus.config.ts b/docusaurus.config.ts
index cb8d5e40..0f4d5478 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
+
+
`,
},
prism: {
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 6ac4bdec..23382a91 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"
},
diff --git a/scripts/update-openapi-index.ts b/scripts/update-openapi-index.ts
new file mode 100644
index 00000000..23276c64
--- /dev/null
+++ b/scripts/update-openapi-index.ts
@@ -0,0 +1,16 @@
+/**
+ * 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';
+
+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/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/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);
}
diff --git a/src/openapi/preprocessing.ts b/src/openapi/preprocessing.ts
index 1e410980..02fd8f20 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)
@@ -28,13 +29,42 @@ 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 = {
+ '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';
// 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"
@@ -408,33 +438,148 @@ async function preprocessOpenApiSpecs() {
console.error(`ā Error processing ${sourcePath}:`, error);
}
- spec.specPath = spec.specPathModified; // Update the original specPath to the modified one
+ // 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');
+};
+
- // Delete the specPathModified property to avoid confusion
- delete spec.specPathModified;
+/**
+ * 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');
+ 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}`);
+ }
+ }
+ }
+ }
+
+ processDirectory(OUTPUT_PREFIX);
+ 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') {
+ // 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);
+ }
+ }
+ return null;
+}
+
+/**
+ * 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...');
+
+ // 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 = '';
+
+ Object.entries(CATEGORY_MAPPING).forEach(([category, specIds]) => {
+ serviceLinksMarkdown += `\n## ${category}\n\n`;
+ specIds.forEach(specId => {
+ categorizedSpecs.add(specId);
+ const spec = specsById.get(specId);
+ if (spec) {
+ const docId = getDocIdFromInfoFile(spec.outputDir);
+ if (docId) {
+ 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`;
+ }
+ }
+ });
+ });
+
+ // 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) {
+ 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`;
+ }
+ });
}
- // Create the index page for OpenAPI documentation
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