From d9adbeaa7d1e4c1fdad0884ff1c27cfcfdb06dd1 Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Mon, 22 Jun 2026 11:15:57 -0600 Subject: [PATCH 1/4] feat(openapi): add usage analytics docs --- apps/web/src/app/api/docs/route.ts | 93 ++++++++ apps/web/src/app/api/openapi.json/route.ts | 5 + apps/web/src/lib/openapi/trpc-openapi.test.ts | 61 ++++++ apps/web/src/lib/openapi/trpc-openapi.ts | 124 +++++++++++ apps/web/src/lib/openapi/trpc-registry.ts | 72 +++++++ .../web/src/routers/usage-analytics-router.ts | 199 ++++-------------- .../src/routers/usage-analytics-schemas.ts | 136 ++++++++++++ 7 files changed, 530 insertions(+), 160 deletions(-) create mode 100644 apps/web/src/app/api/docs/route.ts create mode 100644 apps/web/src/app/api/openapi.json/route.ts create mode 100644 apps/web/src/lib/openapi/trpc-openapi.test.ts create mode 100644 apps/web/src/lib/openapi/trpc-openapi.ts create mode 100644 apps/web/src/lib/openapi/trpc-registry.ts create mode 100644 apps/web/src/routers/usage-analytics-schemas.ts diff --git a/apps/web/src/app/api/docs/route.ts b/apps/web/src/app/api/docs/route.ts new file mode 100644 index 0000000000..ba9469965a --- /dev/null +++ b/apps/web/src/app/api/docs/route.ts @@ -0,0 +1,93 @@ +const swaggerUiHtml = ` + + + + + Kilo Code API Docs + + + + +
+
+

Kilo Code API Docs

+

Swagger UI generated from the allowlisted tRPC OpenAPI document.

+
+ Open JSON +
+
+ + + +`; + +export function GET() { + return new Response(swaggerUiHtml, { + headers: { + 'content-type': 'text/html; charset=utf-8', + }, + }); +} diff --git a/apps/web/src/app/api/openapi.json/route.ts b/apps/web/src/app/api/openapi.json/route.ts new file mode 100644 index 0000000000..0350aa3dee --- /dev/null +++ b/apps/web/src/app/api/openapi.json/route.ts @@ -0,0 +1,5 @@ +import { generateTrpcOpenApiDocument } from '@/lib/openapi/trpc-openapi'; + +export function GET() { + return Response.json(generateTrpcOpenApiDocument()); +} diff --git a/apps/web/src/lib/openapi/trpc-openapi.test.ts b/apps/web/src/lib/openapi/trpc-openapi.test.ts new file mode 100644 index 0000000000..8faa5f9bc7 --- /dev/null +++ b/apps/web/src/lib/openapi/trpc-openapi.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from '@jest/globals'; +import { publicTrpcOpenApiProcedures } from '@/lib/openapi/trpc-registry'; +import { generateTrpcOpenApiDocument } from '@/lib/openapi/trpc-openapi'; + +describe('generateTrpcOpenApiDocument', () => { + it('documents only the allowlisted tRPC procedures', () => { + const document = generateTrpcOpenApiDocument(); + + expect(Object.keys(document.paths).sort()).toEqual( + publicTrpcOpenApiProcedures.map(procedure => procedure.path).sort() + ); + expect(document.paths['/api/trpc/usageAnalytics.getTable']?.post).toMatchObject({ + operationId: 'usageAnalytics_getTable', + summary: 'Get tabular usage analytics', + tags: ['Usage Analytics'], + }); + expect(document.paths).not.toHaveProperty('/api/trpc/admin'); + }); + + it('generates request and response schemas for usageAnalytics.getTable', () => { + const document = generateTrpcOpenApiDocument(); + const operation = document.paths['/api/trpc/usageAnalytics.getTable']?.post; + + expect(operation).toMatchObject({ + requestBody: { + required: true, + content: { + 'application/json': { + schema: { + type: 'object', + required: expect.arrayContaining(['startDate', 'endDate', 'granularity', 'groupBy']), + properties: { + groupBy: { + type: 'array', + items: { enum: ['feature', 'model', 'mode', 'user', 'provider', 'project'] }, + }, + limit: { default: 1000 }, + }, + }, + }, + }, + }, + responses: { + '200': { + content: { + 'application/json': { + schema: { + type: 'object', + required: ['rows', 'effectiveGranularity'], + properties: { + rows: { type: 'array' }, + effectiveGranularity: { enum: ['hour', 'day', 'week', 'month'] }, + }, + }, + }, + }, + }, + }, + }); + }); +}); diff --git a/apps/web/src/lib/openapi/trpc-openapi.ts b/apps/web/src/lib/openapi/trpc-openapi.ts new file mode 100644 index 0000000000..6c6a89fc6f --- /dev/null +++ b/apps/web/src/lib/openapi/trpc-openapi.ts @@ -0,0 +1,124 @@ +import * as z from 'zod'; +import { + publicTrpcOpenApiProcedures, + type TrpcOpenApiProcedure, +} from '@/lib/openapi/trpc-registry'; + +type JsonSchema = Record; + +type OpenApiDocument = { + openapi: '3.1.0'; + info: { + title: string; + version: string; + }; + servers: { url: string }[]; + tags: { name: string }[]; + components: { + securitySchemes: { + apiKey: { + type: 'http'; + scheme: 'bearer'; + }; + }; + }; + paths: Record>; +}; + +function zodToJsonSchema(schema: z.ZodType): JsonSchema { + return z.toJSONSchema(schema, { target: 'draft-7' }) as JsonSchema; +} + +function errorResponse(description: string) { + return { + description, + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + error: { + type: 'object', + properties: { + message: { type: 'string' }, + code: { type: 'number' }, + data: { + type: 'object', + additionalProperties: true, + }, + }, + required: ['message', 'code'], + additionalProperties: true, + }, + }, + required: ['error'], + additionalProperties: true, + }, + }, + }, + }; +} + +function operationForProcedure(procedure: TrpcOpenApiProcedure) { + return { + operationId: procedure.procedurePath.replaceAll('.', '_'), + tags: procedure.tags, + summary: procedure.summary, + description: procedure.description, + security: procedure.security === 'apiKey' ? [{ apiKey: [] }] : undefined, + requestBody: { + required: true, + content: { + 'application/json': { + schema: zodToJsonSchema(procedure.input), + }, + }, + }, + responses: { + '200': { + description: 'Successful tRPC response', + content: { + 'application/json': { + schema: zodToJsonSchema(procedure.output), + }, + }, + }, + '400': errorResponse('Invalid request'), + '401': errorResponse('Authentication required'), + '403': errorResponse('Access denied'), + '500': errorResponse('Unexpected server error'), + }, + }; +} + +export function generateTrpcOpenApiDocument(): OpenApiDocument { + const paths: OpenApiDocument['paths'] = {}; + const tagNames = new Set(); + + for (const procedure of publicTrpcOpenApiProcedures) { + for (const tag of procedure.tags) tagNames.add(tag); + paths[procedure.path] = { + ...paths[procedure.path], + [procedure.method]: operationForProcedure(procedure), + }; + } + + return { + openapi: '3.1.0', + info: { + title: 'Kilo Code tRPC API', + version: '1.0.0', + }, + servers: [{ url: '/' }], + tags: [...tagNames].sort().map(name => ({ name })), + components: { + securitySchemes: { + apiKey: { + type: 'http', + scheme: 'bearer', + }, + }, + }, + paths, + }; +} diff --git a/apps/web/src/lib/openapi/trpc-registry.ts b/apps/web/src/lib/openapi/trpc-registry.ts new file mode 100644 index 0000000000..149fd3da1e --- /dev/null +++ b/apps/web/src/lib/openapi/trpc-registry.ts @@ -0,0 +1,72 @@ +import type * as z from 'zod'; +import { + BreakdownInputSchema, + BreakdownOutputSchema, + SummaryOutputSchema, + TableInputSchema, + TableOutputSchema, + TimeseriesInputSchema, + TimeseriesOutputSchema, + UsageAnalyticsFiltersSchema, +} from '@/routers/usage-analytics-schemas'; + +export type TrpcOpenApiProcedure = { + procedurePath: string; + method: 'post'; + path: `/api/trpc/${string}`; + tags: string[]; + summary: string; + description?: string; + input: z.ZodType; + output: z.ZodType; + security: 'apiKey'; +}; + +export const publicTrpcOpenApiProcedures = [ + { + procedurePath: 'usageAnalytics.getSummary', + method: 'post', + path: '/api/trpc/usageAnalytics.getSummary', + tags: ['Usage Analytics'], + summary: 'Get aggregate usage metrics', + description: + 'Returns aggregate usage metrics for the authenticated user or an accessible organization.', + input: UsageAnalyticsFiltersSchema, + output: SummaryOutputSchema, + security: 'apiKey', + }, + { + procedurePath: 'usageAnalytics.getTimeseries', + method: 'post', + path: '/api/trpc/usageAnalytics.getTimeseries', + tags: ['Usage Analytics'], + summary: 'Get usage over time', + description: + 'Returns usage analytics grouped into time buckets for the authenticated user or organization.', + input: TimeseriesInputSchema, + output: TimeseriesOutputSchema, + security: 'apiKey', + }, + { + procedurePath: 'usageAnalytics.getBreakdown', + method: 'post', + path: '/api/trpc/usageAnalytics.getBreakdown', + tags: ['Usage Analytics'], + summary: 'Get usage breakdown', + description: 'Returns top usage values grouped by a selected dimension.', + input: BreakdownInputSchema, + output: BreakdownOutputSchema, + security: 'apiKey', + }, + { + procedurePath: 'usageAnalytics.getTable', + method: 'post', + path: '/api/trpc/usageAnalytics.getTable', + tags: ['Usage Analytics'], + summary: 'Get tabular usage analytics', + description: 'Returns usage analytics rows grouped by up to three dimensions.', + input: TableInputSchema, + output: TableOutputSchema, + security: 'apiKey', + }, +] satisfies TrpcOpenApiProcedure[]; diff --git a/apps/web/src/routers/usage-analytics-router.ts b/apps/web/src/routers/usage-analytics-router.ts index 6804827e20..35858b2b13 100644 --- a/apps/web/src/routers/usage-analytics-router.ts +++ b/apps/web/src/routers/usage-analytics-router.ts @@ -12,68 +12,45 @@ import { import { kilocode_users, organization_memberships, user_auth_provider } from '@kilocode/db/schema'; import type { AuthProviderId } from '@kilocode/db/schema-types'; import { ensureOrganizationAccess } from '@/routers/organizations/utils'; - -export const GranularitySchema = z.enum(['hour', 'day', 'week', 'month']); -export type Granularity = z.infer; - -export const CostSourceSchema = z.enum(['cost', 'market']); -export type CostSource = z.infer; - -export const DimensionSchema = z.enum(['feature', 'model', 'mode', 'user', 'provider', 'project']); -export type Dimension = z.infer; - -export const MetricSchema = z.enum([ - 'cost', - 'requests', - 'tokens', - 'inputTokens', - 'outputTokens', - 'errorRate', - 'avgLatencyMs', - 'avgGenerationTimeMs', - 'costPerRequest', - 'tokensPerRequest', - 'cacheHitRatio', - 'outputInputRatio', -]); -export type Metric = z.infer; - -const FiltersShape = { - startDate: z.iso.datetime(), - endDate: z.iso.datetime(), - granularity: GranularitySchema, - costSource: CostSourceSchema.default('cost'), - organizationId: z.uuid().optional(), - /** - * Personal-scope narrowing: - * - 'personal-only' (default) → organization_id = '' in Snowflake rollups - * - 'include-orgs' → any organization (including personal) - * Ignored when `organizationId` is set (org scope always filters by that org). - */ - personalScope: z.enum(['personal-only', 'include-orgs']).default('personal-only'), - /** - * Org-scope narrowing when `organizationId` is set: - * - 'self' (default) → restricts to ctx.user.id within the organization - * - 'org-wide' → all users in the org; requires owner/billing_manager - * Ignored when `organizationId` is not set. - */ - viewAs: z.enum(['self', 'org-wide']).default('self'), - features: z.array(z.string()).optional(), - models: z.array(z.string()).optional(), - modes: z.array(z.string()).optional(), - userIds: z.array(z.string()).optional(), - providers: z.array(z.string()).optional(), - projects: z.array(z.string()).optional(), - excludedFeatures: z.array(z.string()).optional(), - excludedModels: z.array(z.string()).optional(), - excludedModes: z.array(z.string()).optional(), - excludedUserIds: z.array(z.string()).optional(), - excludedProviders: z.array(z.string()).optional(), - excludedProjects: z.array(z.string()).optional(), -} as const; - -export const UsageAnalyticsFiltersSchema = z.object(FiltersShape); -export type UsageAnalyticsFilters = z.infer; +import { + BreakdownInputSchema, + BreakdownOutputSchema, + SummaryOutputSchema, + TableInputSchema, + TableOutputSchema, + TimeseriesInputSchema, + TimeseriesOutputSchema, + UsageAnalyticsFiltersSchema, + type CostSource, + type Dimension, + type Granularity, + type Metric, + type SummaryOutput, + type UsageAnalyticsFilters, +} from '@/routers/usage-analytics-schemas'; + +export { + BreakdownInputSchema, + BreakdownOutputSchema, + CostSourceSchema, + DimensionSchema, + GranularitySchema, + MetricSchema, + SummaryOutputSchema, + TableInputSchema, + TableOutputSchema, + TimeseriesInputSchema, + TimeseriesOutputSchema, + UsageAnalyticsFiltersSchema, +} from '@/routers/usage-analytics-schemas'; +export type { + CostSource, + Dimension, + Granularity, + Metric, + SummaryOutput, + UsageAnalyticsFilters, +} from '@/routers/usage-analytics-schemas'; // --------------------------------------------------------------------------- // Table / tier resolution @@ -532,35 +509,6 @@ async function timedSnowflakeQuery( // getSummary // --------------------------------------------------------------------------- -const SummaryOutputSchema = z.object({ - costMicrodollars: z.number(), - requestCount: z.number(), - inputTokens: z.number(), - outputTokens: z.number(), - cacheWriteTokens: z.number(), - cacheHitTokens: z.number(), - errorCount: z.number(), - cancelledCount: z.number(), - freeRequestCount: z.number(), - byokRequestCount: z.number(), - totalLatencyMs: z.number(), - totalGenerationTimeMs: z.number(), - latencyCount: z.number(), - generationTimeCount: z.number(), - totalTokens: z.number(), - distinctUsers: z.number(), - errorRate: z.number(), - avgLatencyMs: z.number(), - avgGenerationTimeMs: z.number(), - costPerRequest: z.number(), - tokensPerRequest: z.number(), - cacheHitRatio: z.number(), - outputInputRatio: z.number(), - effectiveGranularity: GranularitySchema, -}); - -type SummaryOutput = z.infer; - function ratioSafe(numerator: number, denominator: number): number { if (denominator === 0) return 0; return numerator / denominator; @@ -582,75 +530,6 @@ function toSafeNumber(value: unknown): number { return n; } -// --------------------------------------------------------------------------- -// Timeseries -// --------------------------------------------------------------------------- - -const TimeseriesInputSchema = UsageAnalyticsFiltersSchema.extend({ - metric: MetricSchema, - splitBy: DimensionSchema.optional(), -}); - -const TimeseriesPointSchema = z.object({ - datetime: z.string(), - value: z.number(), - label: z.string().optional(), -}); - -const TimeseriesOutputSchema = z.object({ - timeseries: z.array(TimeseriesPointSchema), - effectiveGranularity: GranularitySchema, -}); - -// --------------------------------------------------------------------------- -// Breakdown -// --------------------------------------------------------------------------- - -const BreakdownInputSchema = UsageAnalyticsFiltersSchema.extend({ - dimension: DimensionSchema, - metric: z.enum(['cost', 'requests', 'tokens']), - limit: z.number().int().min(1).max(100).default(15), -}); - -const BreakdownItemSchema = z.object({ - key: z.string(), - label: z.string(), - value: z.number(), - percentage: z.number(), -}); - -const BreakdownOutputSchema = z.object({ - breakdown: z.array(BreakdownItemSchema), - totalValue: z.number(), - effectiveGranularity: GranularitySchema, -}); - -// --------------------------------------------------------------------------- -// Table -// --------------------------------------------------------------------------- - -const TableInputSchema = UsageAnalyticsFiltersSchema.extend({ - groupBy: z.array(DimensionSchema).max(3), - limit: z.number().int().min(1).max(10_000).default(1000), -}); - -const TableRowSchema = z.object({ - datetime: z.string(), - dimensions: z.record(z.string(), z.string()), - costMicrodollars: z.number(), - requestCount: z.number(), - inputTokens: z.number(), - outputTokens: z.number(), - cacheWriteTokens: z.number(), - cacheHitTokens: z.number(), - errorCount: z.number(), -}); - -const TableOutputSchema = z.object({ - rows: z.array(TableRowSchema), - effectiveGranularity: GranularitySchema, -}); - // --------------------------------------------------------------------------- // User list (for org context) // --------------------------------------------------------------------------- diff --git a/apps/web/src/routers/usage-analytics-schemas.ts b/apps/web/src/routers/usage-analytics-schemas.ts new file mode 100644 index 0000000000..0c38fb1f06 --- /dev/null +++ b/apps/web/src/routers/usage-analytics-schemas.ts @@ -0,0 +1,136 @@ +import * as z from 'zod'; + +export const GranularitySchema = z.enum(['hour', 'day', 'week', 'month']); +export type Granularity = z.infer; + +export const CostSourceSchema = z.enum(['cost', 'market']); +export type CostSource = z.infer; + +export const DimensionSchema = z.enum(['feature', 'model', 'mode', 'user', 'provider', 'project']); +export type Dimension = z.infer; + +export const MetricSchema = z.enum([ + 'cost', + 'requests', + 'tokens', + 'inputTokens', + 'outputTokens', + 'errorRate', + 'avgLatencyMs', + 'avgGenerationTimeMs', + 'costPerRequest', + 'tokensPerRequest', + 'cacheHitRatio', + 'outputInputRatio', +]); +export type Metric = z.infer; + +const FiltersShape = { + startDate: z.iso.datetime(), + endDate: z.iso.datetime(), + granularity: GranularitySchema, + costSource: CostSourceSchema.default('cost'), + organizationId: z.uuid().optional(), + personalScope: z.enum(['personal-only', 'include-orgs']).default('personal-only'), + viewAs: z.enum(['self', 'org-wide']).default('self'), + features: z.array(z.string()).optional(), + models: z.array(z.string()).optional(), + modes: z.array(z.string()).optional(), + userIds: z.array(z.string()).optional(), + providers: z.array(z.string()).optional(), + projects: z.array(z.string()).optional(), + excludedFeatures: z.array(z.string()).optional(), + excludedModels: z.array(z.string()).optional(), + excludedModes: z.array(z.string()).optional(), + excludedUserIds: z.array(z.string()).optional(), + excludedProviders: z.array(z.string()).optional(), + excludedProjects: z.array(z.string()).optional(), +} as const; + +export const UsageAnalyticsFiltersSchema = z.object(FiltersShape); +export type UsageAnalyticsFilters = z.infer; + +export const SummaryOutputSchema = z.object({ + costMicrodollars: z.number(), + requestCount: z.number(), + inputTokens: z.number(), + outputTokens: z.number(), + cacheWriteTokens: z.number(), + cacheHitTokens: z.number(), + errorCount: z.number(), + cancelledCount: z.number(), + freeRequestCount: z.number(), + byokRequestCount: z.number(), + totalLatencyMs: z.number(), + totalGenerationTimeMs: z.number(), + latencyCount: z.number(), + generationTimeCount: z.number(), + totalTokens: z.number(), + distinctUsers: z.number(), + errorRate: z.number(), + avgLatencyMs: z.number(), + avgGenerationTimeMs: z.number(), + costPerRequest: z.number(), + tokensPerRequest: z.number(), + cacheHitRatio: z.number(), + outputInputRatio: z.number(), + effectiveGranularity: GranularitySchema, +}); +export type SummaryOutput = z.infer; + +export const TimeseriesInputSchema = UsageAnalyticsFiltersSchema.extend({ + metric: MetricSchema, + splitBy: DimensionSchema.optional(), +}); + +const TimeseriesPointSchema = z.object({ + datetime: z.string(), + value: z.number(), + label: z.string().optional(), +}); + +export const TimeseriesOutputSchema = z.object({ + timeseries: z.array(TimeseriesPointSchema), + effectiveGranularity: GranularitySchema, +}); + +export const BreakdownInputSchema = UsageAnalyticsFiltersSchema.extend({ + dimension: DimensionSchema, + metric: z.enum(['cost', 'requests', 'tokens']), + limit: z.number().int().min(1).max(100).default(15), +}); + +const BreakdownItemSchema = z.object({ + key: z.string(), + label: z.string(), + value: z.number(), + percentage: z.number(), +}); + +export const BreakdownOutputSchema = z.object({ + breakdown: z.array(BreakdownItemSchema), + totalValue: z.number(), + effectiveGranularity: GranularitySchema, +}); + +export const TableInputSchema = UsageAnalyticsFiltersSchema.extend({ + groupBy: z.array(DimensionSchema).max(3), + limit: z.number().int().min(1).max(10_000).default(1000), +}); + +const TableRowSchema = z.object({ + datetime: z.string(), + dimensions: z.record(z.string(), z.string()), + costMicrodollars: z.number(), + requestCount: z.number(), + inputTokens: z.number(), + outputTokens: z.number(), + cacheWriteTokens: z.number(), + cacheHitTokens: z.number(), + errorCount: z.number(), +}); + +export const TableOutputSchema = z.object({ + rows: z.array(TableRowSchema), + effectiveGranularity: GranularitySchema, +}); From 9cf3d35ffd19534405dd19607fab0ff9dfe5bdac Mon Sep 17 00:00:00 2001 From: Evan Jacobson Date: Mon, 22 Jun 2026 11:37:36 -0600 Subject: [PATCH 2/4] fix(openapi): harden swagger docs --- apps/web/package.json | 1 + apps/web/src/app/api/docs/[asset]/route.ts | 35 +++++++++++++++++++ apps/web/src/app/api/docs/route.ts | 34 ++++++++++++++---- apps/web/src/app/api/openapi.json/route.ts | 8 ++++- apps/web/src/lib/openapi/trpc-openapi.test.ts | 20 ++++++++--- apps/web/src/lib/openapi/trpc-openapi.ts | 29 +++++++++++++-- apps/web/src/lib/openapi/trpc-registry.ts | 5 --- pnpm-lock.yaml | 15 ++++++++ pnpm-workspace.yaml | 1 + 9 files changed, 128 insertions(+), 20 deletions(-) create mode 100644 apps/web/src/app/api/docs/[asset]/route.ts diff --git a/apps/web/package.json b/apps/web/package.json index 98005b3306..ef579fcad6 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -162,6 +162,7 @@ "sonner": "2.0.7", "stripe": "catalog:", "stytch": "12.43.1", + "swagger-ui-dist": "5.32.6", "tailwind-merge": "3.5.0", "tldts": "7.0.30", "ulid": "catalog:", diff --git a/apps/web/src/app/api/docs/[asset]/route.ts b/apps/web/src/app/api/docs/[asset]/route.ts new file mode 100644 index 0000000000..c15059909c --- /dev/null +++ b/apps/web/src/app/api/docs/[asset]/route.ts @@ -0,0 +1,35 @@ +import { readFile } from 'node:fs/promises'; +import { createRequire } from 'node:module'; +import { join } from 'node:path'; + +const require = createRequire(import.meta.url); +const swaggerUiDist = require('swagger-ui-dist') as { getAbsoluteFSPath: () => string }; + +const assetContentTypes = { + 'swagger-ui-bundle.js': 'text/javascript; charset=utf-8', + 'swagger-ui.css': 'text/css; charset=utf-8', +} as const; + +function contentTypeForAsset(asset: string) { + if (asset === 'swagger-ui-bundle.js') return assetContentTypes[asset]; + if (asset === 'swagger-ui.css') return assetContentTypes[asset]; + return null; +} + +export async function GET(_request: Request, { params }: { params: Promise<{ asset: string }> }) { + const { asset } = await params; + const contentType = contentTypeForAsset(asset); + + if (!contentType) { + return new Response('Not found', { status: 404 }); + } + + const content = await readFile(join(swaggerUiDist.getAbsoluteFSPath(), asset)); + + return new Response(content, { + headers: { + 'cache-control': 'public, max-age=31536000, immutable', + 'content-type': contentType, + }, + }); +} diff --git a/apps/web/src/app/api/docs/route.ts b/apps/web/src/app/api/docs/route.ts index ba9469965a..202fb18616 100644 --- a/apps/web/src/app/api/docs/route.ts +++ b/apps/web/src/app/api/docs/route.ts @@ -1,11 +1,14 @@ -const swaggerUiHtml = ` +import { randomBytes } from 'node:crypto'; + +function swaggerUiHtml(nonce: string) { + return ` Kilo Code API Docs - -