Ctrl K
ComunidadGuías…

server/http/openapi/management.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
import {
  accessRequestSchema,
  agentStatusSchema,
  createAccessRequestSchema,
  resourceServerAuthorizationDetailsResponseSchema,
} from '@shared/api/agent-api'
import { agentPublicIdentifierSchema } from '@shared/api/identifiers'
import { realmrootApiVersion } from '@shared/api/openapi'
import { paginationQuerySchema } from '@shared/api/pagination'
import {
  publicAgentResponseSchema,
  publicProfileQuerySchema,
  publicUserResponseSchema,
} from '@shared/api/public-profiles'
import { usernameSchema } from '@shared/api/users'
import { agentBootstrapScopes, realmrootOAuthScopes, requiredProtectedScope } from '@shared/authz'
import { realmrootManagementScopes } from '@shared/scope-registry'
import { z } from 'zod'
import { agentGovernanceRoutes } from './management-routes/agent-governance'
import { applicationAuthorizationRoutes } from './management-routes/applications-authorization'
import {
  errorResponse,
  interactiveResourceResponseHeaders,
  jsonBody,
  jsonContentType,
  locationResponseHeader,
  type ManagementRouteConfig,
  paginationResponseHeader,
  uploadedAssetResponseSchema,
} from './management-routes/helpers'
import { platformRuntimeRoutes } from './management-routes/platform-runtime'
import { platformWebhookRoutes } from './management-routes/platform-webhooks'
import { userSecurityRoutes } from './management-routes/users-security'

interface UnifiedOpenApiDocument {
  openapi: string
  info: unknown
  paths: Record<string, unknown>
  components: {
    securitySchemes: Record<string, unknown>
    parameters?: Record<string, unknown>
    pathItems?: Record<string, unknown>
    responses?: Record<string, unknown>
    schemas?: Record<string, unknown>
  }
  security?: unknown
  'x-cli-config': RestishCliConfig
  [key: string]: unknown
}
interface RestishCliConfig {
  command_layout: 'tags'
}

const managementOpenApiTags = [
  { name: 'Assets', description: 'Uploaded assets used by Realmroot resources.' },
  { name: 'Agent', description: 'Authenticated Agent bootstrap and access protocol resources.' },
  { name: 'Agents', description: 'Agent identities, installations, and lifecycle.' },
  { name: 'Applications', description: 'OIDC and machine-to-machine client applications.' },
  { name: 'Resource Servers', description: 'Protected Resource Servers, contracts, and connections.' },
  { name: 'Organizations', description: 'Organizations, memberships, invitations, and roles.' },
  { name: 'Users', description: 'Realmroot users and account security resources.' },
  { name: 'Connectors', description: 'External identity and resource connectors.' },
  { name: 'Webhooks', description: 'Webhook endpoints, deliveries, attempts, and secrets.' },
  { name: 'Public Profiles', description: 'Public representations of User and Agent identities.' },
  { name: 'Platform', description: 'Realm-wide resources, service health, and operational metadata.' },
] as const

const publicProfileResponseHeaders = {
  ETag: { description: 'Validator for the selected public representation.', schema: { type: 'string' } },
  'Cache-Control': {
    description: 'Shared-cache policy for the selected public representation.',
    schema: { type: 'string' },
  },
}

const publicProfileRequestHeaders = z.object({
  'If-None-Match': z
    .string()
    .optional()
    .openapi({
      param: { name: 'If-None-Match', in: 'header' },
      example: '"representation-version"',
    }),
})

const publicProfileAdditionalResponses = {
  304: {
    description: 'The selected public representation has not changed.',
    headers: publicProfileResponseHeaders,
  },
}

const managementRoutes: ManagementRouteConfig[] = [
  {
    method: 'get',
    path: '/public/users/{username}',
    operationId: 'getPublicUser',
    summary: 'Get a public User representation',
    security: [],
    request: {
      params: z.object({ username: usernameSchema }),
      query: publicProfileQuerySchema,
      headers: publicProfileRequestHeaders,
    },
    response: publicUserResponseSchema,
    responseHeaders: publicProfileResponseHeaders,
    additionalResponses: publicProfileAdditionalResponses,
    errors: { 400: 'The username or view is invalid.', 404: 'The public User was not found.' },
  },
  {
    method: 'get',
    path: '/public/agents/{subject}',
    operationId: 'getPublicAgent',
    summary: 'Get a public Agent representation',
    security: [],
    request: {
      params: z.object({ subject: agentPublicIdentifierSchema }),
      query: publicProfileQuerySchema,
      headers: publicProfileRequestHeaders,
    },
    response: publicAgentResponseSchema,
    responseHeaders: publicProfileResponseHeaders,
    additionalResponses: publicProfileAdditionalResponses,
    errors: { 400: 'The Agent identifier or view is invalid.', 404: 'The public Agent was not found.' },
  },
  {
    method: 'post',
    path: '/assets',
    operationId: 'createAsset',
    summary: 'Create an Asset',
    request: {
      body: {
        content: {
          'multipart/form-data': {
            schema: z.object({
              purpose: z.enum(['avatar', 'application_logo', 'organization_logo', 'branding_logo', 'favicon']),
              file: z.string().openapi({ format: 'binary' }),
            }),
          },
        },
      },
    },
    response: uploadedAssetResponseSchema,
    status: 201,
    responseHeaders: locationResponseHeader,
  },
  {
    method: 'get',
    path: '/assets/{assetId}',
    operationId: 'getAsset',
    summary: 'Get an Asset',
    security: [],
    request: { params: z.object({ assetId: z.string() }) },
    response: z.unknown(),
  },
  {
    method: 'get',
    path: '/agent',
    operationId: 'getAgentStatus',
    summary: 'Read the current Agent status',
    cli: { group: 'Agent', name: 'whoami' },
    security: [{ oauth2: ['agent:read'] }],
    response: agentStatusSchema,
  },
  {
    method: 'get',
    path: '/resource-servers/{resourceServerId}/authorization-details',
    operationId: 'listResourceServerAuthorizationDetails',
    summary: 'List authorization details available through a Resource Server',
    security: [{ oauth2: ['authorization-details:read'] }],
    request: {
      params: z.object({ resourceServerId: z.string() }),
      query: paginationQuerySchema,
    },
    response: resourceServerAuthorizationDetailsResponseSchema,
    paginated: true,
  },
  {
    method: 'post',
    path: '/agent/access-requests',
    operationId: 'createAgentAuthorizationRequest',
    summary: 'Create an Agent authorization request',
    cli: { name: 'access' },
    security: [{ oauth2: ['access-requests:read', 'access-requests:write'] }],
    request: { body: jsonBody(createAccessRequestSchema) },
    response: accessRequestSchema,
    status: 201,
    responseHeaders: { ...locationResponseHeader, ...interactiveResourceResponseHeaders },
  },
  {
    method: 'get',
    path: '/agent/access-requests/{requestId}',
    operationId: 'getAgentAuthorizationRequest',
    summary: "Get the authenticated Agent's authorization request",
    security: [{ oauth2: ['access-requests:read'] }],
    request: { params: z.object({ requestId: z.string() }) },
    response: accessRequestSchema,
    responseHeaders: interactiveResourceResponseHeaders,
    errors: { 404: 'The Agent access request was not found.' },
  },
  ...agentGovernanceRoutes,
  ...applicationAuthorizationRoutes,
  ...userSecurityRoutes,
  ...platformWebhookRoutes,
  ...platformRuntimeRoutes,
]
let cachedOpenApi: UnifiedOpenApiDocument | undefined
export function getUnifiedOpenApi(): UnifiedOpenApiDocument {
  cachedOpenApi ??= buildUnifiedOpenApi()
  return cachedOpenApi
}

function createManagementOpenApiApp() {
  const app = new OpenAPIHono()
  app.openAPIRegistry.registerComponent('securitySchemes', 'sessionCookie', {
    type: 'apiKey',
    in: 'cookie',
    name: 'better-auth.session_token',
    description: 'Authenticated browser session; each operation applies Realm, Organization, or account visibility.',
  })
  app.openAPIRegistry.registerComponent('securitySchemes', 'agentAssertion', {
    type: 'http',
    scheme: 'bearer',
    description: 'Short-lived Agent protocol assertion used only to create and read Agent enrollments.',
  })
  app.openAPIRegistry.registerComponent('securitySchemes', 'oauth2', {
    type: 'oauth2',
    flows: {
      authorizationCode: {
        authorizationUrl: '/api/auth/oauth2/authorize',
        tokenUrl: '/api/auth/oauth2/token',
        scopes: Object.fromEntries(realmrootOAuthScopes.map((scope) => [scope, oauthScopeDescription(scope)])),
      },
      clientCredentials: {
        tokenUrl: '/api/auth/oauth2/token',
        scopes: Object.fromEntries(realmrootManagementScopes.map((scope) => [scope, oauthScopeDescription(scope)])),
      },
    },
    description: 'Realmroot OAuth 2.0 Bearer credential for User or Application access; Agent tokens use DPoP.',
  })
  for (const routeConfig of managementRoutes) app.openAPIRegistry.registerPath(createManagementRoute(routeConfig))
  return app
}

function buildUnifiedOpenApi(): UnifiedOpenApiDocument {
  const document = createManagementOpenApiApp().getOpenAPI31Document(
    {
      openapi: '3.1.0',
      info: {
        title: 'Realmroot API',
        version: realmrootApiVersion,
        description:
          'Unified API for Agent identity, self-service resources, and permission-gated tenant administration.',
      },
      tags: [...managementOpenApiTags],
      servers: [{ url: '/api' }],
    },
    { unionPreferredType: 'oneOf' },
  )
  return {
    ...document,
    'x-cli-config': {
      command_layout: 'tags',
    },
  } as UnifiedOpenApiDocument
}

function createManagementRoute(routeConfig: ManagementRouteConfig) {
  const requiredScope =
    routeConfig.security === undefined
      ? requiredProtectedScope(routeConfig.method.toUpperCase(), routeConfig.path)
      : null
  return createRoute({
    method: routeConfig.method,
    path: routeConfig.path,
    operationId: routeConfig.operationId,
    summary: routeConfig.summary,
    tags: [routeConfig.cli?.group ?? managementTagForPath(routeConfig.path)],
    ...(routeConfig.cli
      ? {
          'x-cli-name': routeConfig.cli.name,
        }
      : { 'x-cli-hidden': true }),
    security:
      routeConfig.security ??
      (requiredScope ? [{ oauth2: [requiredScope] }, { sessionCookie: [requiredScope] }] : [{ sessionCookie: [] }]),
    request: routeConfig.request as never,
    responses: routeResponses(routeConfig) as never,
  })
}

function oauthScopeDescription(scope: string) {
  return agentBootstrapScopes.includes(scope as (typeof agentBootstrapScopes)[number])
    ? `Realmroot Agent protocol scope: ${scope}`
    : `Realmroot Resource management scope: ${scope}`
}

function managementTagForPath(path: string): (typeof managementOpenApiTags)[number]['name'] {
  if (path.startsWith('/public/')) return 'Public Profiles'
  if (path.startsWith('/assets')) return 'Assets'
  if (path === '/agent' || path.startsWith('/agent/')) return 'Agent'
  if (path.startsWith('/agents')) return 'Agents'
  if (path.startsWith('/applications')) return 'Applications'
  if (path.startsWith('/resource-servers')) return 'Resource Servers'
  if (path.startsWith('/organizations')) return 'Organizations'
  if (path.startsWith('/users')) return 'Users'
  if (path.startsWith('/connectors')) return 'Connectors'
  if (path === '/realm' || path.startsWith('/realm/')) return 'Platform'
  if (path.startsWith('/webhooks')) return 'Webhooks'
  throw new Error(`Management OpenAPI route has no domain tag: ${path}`)
}

function routeResponses(routeConfig: ManagementRouteConfig) {
  const responses: Record<string, unknown> = {}
  const responseHeaders = {
    ...(routeConfig.paginated ? paginationResponseHeader : {}),
    ...routeConfig.responseHeaders,
  }
  if (routeConfig.noBody)
    responses[routeConfig.status ?? 204] = {
      description: routeConfig.summary,
      ...(Object.keys(responseHeaders).length > 0 ? { headers: responseHeaders } : {}),
    }
  else
    responses[routeConfig.status ?? 200] = {
      description: routeConfig.summary,
      ...(Object.keys(responseHeaders).length > 0 ? { headers: responseHeaders } : {}),
      content: { [jsonContentType]: { schema: routeConfig.response } },
    }
  const expectedErrors = Object.fromEntries(
    Object.entries(routeConfig.errors ?? {}).map(([status, error]) => [
      status,
      typeof error === 'string' ? errorResponse(error) : errorResponse(error.description, error.schema, error.headers),
    ]),
  )
  const additionalResponses = routeConfig.additionalResponses ?? {}
  if (routeConfig.security !== undefined && routeConfig.security.length === 0) {
    return { ...responses, ...additionalResponses, ...expectedErrors }
  }
  return {
    ...responses,
    ...additionalResponses,
    401: errorResponse('Authentication is required.'),
    403: errorResponse('Administrator access is required.'),
    ...expectedErrors,
  }
}