* fix(automation): align public API plan gating across cloud and self-hosted Add shared canUsePublicApi helper so cloud allows Early Adopter and Enterprise while self-hosted requires Enterprise. Update the API key dashboard page and backend key creation checks to use the same rules. Co-authored-by: Rotimi Best <[email protected]> * docs(api): document bearer auth on public API reference Add shared OpenAPI enrichment with bearerAuth security scheme and authentication instructions. The interactive reference now loads an enriched spec with Authorize support, and the API guide explains how to create and use org API keys. Co-authored-by: Rotimi Best <[email protected]> * feat(dashboard): add API quick-start code snippets on automation page Mirror the MCP client setup section with cURL, JavaScript, and Python examples that show how to authenticate public API requests with org keys. Co-authored-by: Rotimi Best <[email protected]> * ci(openapi): republish spec when public API OpenAPI helpers change Co-authored-by: Rotimi Best <[email protected]> * feat(dashboard): add Go snippet to API quick-start examples Co-authored-by: Rotimi Best <[email protected]> * docs(api): use /org/*/api and clarify dashboard key generation path Co-authored-by: Rotimi Best <[email protected]> * fix(api): escape backticks in OpenAPI description template literal Greptile flagged that unescaped backticks around * in PUBLIC_API_OPENAPI_DESCRIPTION evaluated to NaN and dropped auth docs from the Scalar spec. Also use plan-specific snackbar message on upgrade. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]>
214 lines
5.9 KiB
TypeScript
214 lines
5.9 KiB
TypeScript
#!/usr/bin/env tsx
|
|
|
|
import 'dotenv/config';
|
|
|
|
import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3';
|
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
|
|
import { app } from '../src/app';
|
|
import { env } from '@cio/core/config/env';
|
|
import { generateSpecs } from 'hono-openapi';
|
|
import { join } from 'path';
|
|
import { enrichPublicApiOpenApiSpec } from '@cio/utils/openapi/public-api';
|
|
|
|
interface UploadOptions {
|
|
key: string;
|
|
content: string;
|
|
contentType?: string;
|
|
}
|
|
|
|
function filterPublicApiSpec(spec: Record<string, unknown>) {
|
|
const paths = (spec.paths ?? {}) as Record<string, unknown>;
|
|
const publicApiPaths = Object.fromEntries(
|
|
Object.entries(paths).filter(([path]) => path.startsWith('/public-api/v1/'))
|
|
);
|
|
|
|
const tags = Array.isArray(spec.tags)
|
|
? spec.tags.filter((tag) => {
|
|
if (!tag || typeof tag !== 'object' || !('name' in tag)) {
|
|
return false;
|
|
}
|
|
|
|
return typeof tag.name === 'string' && tag.name.startsWith('Public API');
|
|
})
|
|
: undefined;
|
|
|
|
return enrichPublicApiOpenApiSpec({
|
|
...spec,
|
|
openapi: spec.openapi ?? '3.1.0',
|
|
paths: publicApiPaths,
|
|
tags
|
|
});
|
|
}
|
|
|
|
class OpenAPISpecGenerator {
|
|
private s3Client: S3Client | null = null;
|
|
|
|
constructor() {
|
|
this.initializeS3Client();
|
|
}
|
|
|
|
private initializeS3Client() {
|
|
if (env.CLOUDFLARE_ACCESS_KEY && env.CLOUDFLARE_SECRET_ACCESS_KEY && env.CLOUDFLARE_ACCOUNT_ID) {
|
|
this.s3Client = new S3Client({
|
|
region: 'auto',
|
|
endpoint: `https://${env.CLOUDFLARE_ACCOUNT_ID}.r2.cloudflarestorage.com`,
|
|
credentials: {
|
|
accessKeyId: env.CLOUDFLARE_ACCESS_KEY,
|
|
secretAccessKey: env.CLOUDFLARE_SECRET_ACCESS_KEY
|
|
}
|
|
});
|
|
console.log('✅ S3 client initialized for Cloudflare R2');
|
|
} else {
|
|
console.warn('⚠️ Cloudflare R2 credentials not found. Will only generate local spec file.');
|
|
}
|
|
}
|
|
|
|
async generateSpec(): Promise<string> {
|
|
try {
|
|
console.log('🔄 Generating OpenAPI specification...');
|
|
|
|
// Generate the OpenAPI spec from the Hono app
|
|
const spec = await generateSpecs(app, {
|
|
documentation: {
|
|
info: {
|
|
title: 'ClassroomIO API',
|
|
version: '1.0.0',
|
|
description: 'Manage your organization on classroomio via the API',
|
|
contact: {
|
|
name: 'ClassroomIO',
|
|
url: 'https://classroomio.com'
|
|
}
|
|
},
|
|
servers: [
|
|
{
|
|
url: 'https://api.classroomio.com',
|
|
description: 'Production environment'
|
|
},
|
|
{
|
|
url: 'http://localhost:3002',
|
|
description: 'Development environment'
|
|
}
|
|
]
|
|
}
|
|
});
|
|
|
|
const publicApiSpec = filterPublicApiSpec(spec as Record<string, unknown>);
|
|
const specString = JSON.stringify(publicApiSpec, null, 2);
|
|
console.log('✅ OpenAPI specification generated successfully');
|
|
|
|
return specString;
|
|
} catch (error) {
|
|
console.error('❌ Error generating OpenAPI spec:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async saveLocalSpec(spec: string): Promise<string> {
|
|
try {
|
|
const outputDir = join(process.cwd(), 'dist', 'openapi', 'public-api');
|
|
mkdirSync(outputDir, { recursive: true });
|
|
|
|
const filePath = join(outputDir, 'openapi.json');
|
|
writeFileSync(filePath, spec, 'utf8');
|
|
|
|
console.log(`✅ OpenAPI spec saved locally: ${filePath}`);
|
|
return filePath;
|
|
} catch (error) {
|
|
console.error('❌ Error saving local spec:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async uploadToR2(options: UploadOptions): Promise<void> {
|
|
if (!this.s3Client) {
|
|
throw new Error('S3 client not initialized. Check your Cloudflare R2 credentials.');
|
|
}
|
|
|
|
try {
|
|
console.log(`🔄 Uploading to R2 bucket`);
|
|
|
|
const command = new PutObjectCommand({
|
|
Bucket: 'api',
|
|
Key: options.key,
|
|
Body: options.content,
|
|
ContentType: options.contentType || 'application/json',
|
|
CacheControl: 'public, max-age=3600' // Cache for 1 hour
|
|
});
|
|
|
|
await this.s3Client.send(command);
|
|
console.log(`✅ Successfully uploaded to R2`);
|
|
} catch (error) {
|
|
console.error('❌ Error uploading to R2:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async generateAndUpload(): Promise<void> {
|
|
try {
|
|
const spec = await this.generateSpec();
|
|
|
|
await this.saveLocalSpec(spec);
|
|
|
|
if (this.s3Client) {
|
|
const uploadKey = `openapi/public-api/openapi-${new Date().toISOString().split('T')[0]}.json`;
|
|
await this.uploadToR2({
|
|
key: uploadKey,
|
|
content: spec,
|
|
contentType: 'application/json'
|
|
});
|
|
|
|
// Also upload as latest version
|
|
await this.uploadToR2({
|
|
key: 'openapi/public-api/openapi-latest.json',
|
|
content: spec,
|
|
contentType: 'application/json'
|
|
});
|
|
}
|
|
|
|
console.log('🎉 OpenAPI spec generation and upload completed successfully!');
|
|
} catch (error) {
|
|
console.error('❌ Failed to generate and upload OpenAPI spec:', error);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
// CLI interface
|
|
async function main() {
|
|
const args = process.argv.slice(2);
|
|
|
|
if (args.includes('--help') || args.includes('-h')) {
|
|
console.log(`
|
|
Usage: tsx scripts/upload-openapi-spec.ts
|
|
|
|
Examples:
|
|
tsx scripts/upload-openapi-spec.ts
|
|
|
|
Environment Variables Required for R2 Upload:
|
|
CLOUDFLARE_ACCESS_KEY
|
|
CLOUDFLARE_SECRET_ACCESS_KEY
|
|
CLOUDFLARE_ACCOUNT_ID
|
|
`);
|
|
return;
|
|
}
|
|
|
|
const generator = new OpenAPISpecGenerator();
|
|
await generator.generateAndUpload();
|
|
}
|
|
|
|
// Run the script if called directly
|
|
if (process.argv[1] && process.argv[1].endsWith('upload-openapi-spec.ts')) {
|
|
main()
|
|
.then(() => {
|
|
console.log('Script completed successfully');
|
|
process.exit(0);
|
|
})
|
|
.catch((error) => {
|
|
console.error('Script failed:', error);
|
|
process.exit(1);
|
|
});
|
|
}
|
|
|
|
export { OpenAPISpecGenerator };
|