import type { BaseLogger } from 'pino'; import { RegistrationResponseSchema, type OpenIDConfiguration, type RegistrationResponse, } from '../../schemas/index.js'; import { escapeHtml } from '../../utils/htmlEscaping.js'; import { getAGSScopes, hasAGSSupport, hasDeepLinkingSupport, hasNRPSSupport, } from '../../utils/ltiPlatformCapabilities.js'; import { ltiServiceFetch } from '../../utils/ltiServiceFetch.js'; /** * Generates a generic dynamic registration form with service selection options. * Creates a Bootstrap 5 form that detects available LTI Advantage services from the platform * configuration and presents them as selectable checkboxes to the administrator. * * @param openIdConfiguration - Platform's OpenID Connect configuration containing supported services * @param currentPath - Current request path used to build the form submission URL * @param sessionToken - Security token for CSRF protection and session validation * @returns Complete HTML page with Bootstrap form for service selection * * @example * ```typescript * const html = renderDynamicRegistrationForm( * platformConfig, * '/lti/register', * 'uuid-session-token' * ); * // Returns HTML form with AGS, NRPS, and Deep Linking options if supported * ``` */ // oxlint-disable-next-line max-lines-per-function export function renderDynamicRegistrationForm( openIdConfiguration: OpenIDConfiguration, currentPath: string, sessionToken: string, ): string { const hasAGS = hasAGSSupport(openIdConfiguration); const hasNRPS = hasNRPSSupport(openIdConfiguration); const hasDeepLinking = hasDeepLinkingSupport(openIdConfiguration); const agsScopes = getAGSScopes(openIdConfiguration); // Build complete action from current path const completeAction = `${currentPath}/complete`; return ` Configure LTI Advantage Settings
${ hasAGS ? `
OAuth Scopes that will be requested:
${agsScopes.map((scope) => scope.replace('https://purl.imsglobal.org/spec/lti-ags/scope/', '')).join('\n')}
` : '' } ${ hasNRPS ? `
` : '' } ${ hasDeepLinking ? `
` : '' }
These privacy settings must be enabled in your LMS for this tool to function properly.
`; } /** * Submits tool registration payload to a platform's dynamic registration endpoint. * Handles the HTTP communication with proper authentication, error handling, and response validation. * Validates the registration response against the LTI 1.3 specification schema. * * @param registrationEndpoint - Platform's registration endpoint URL from OpenID configuration * @param registrationPayload - Complete tool registration payload with OAuth and LTI configuration * @param logger - Pino logger instance for request/response logging and error tracking * @param registrationToken - Optional bearer token for authenticated registration requests * @returns Validated registration response containing client credentials and deployment information * @throws {Error} When registration request fails or response validation fails * * @example * ```typescript * const response = await postRegistrationToPlatform( * 'https://platform.example/registration', * registrationPayload, * logger, * 'optional-bearer-token' * ); * console.log('Registered with client ID:', response.client_id); * ``` */ export async function postRegistrationToPlatform( registrationEndpoint: string, registrationPayload: unknown, logger: BaseLogger, registrationToken?: string, ): Promise { const headers: Record = { 'Content-Type': 'application/json' }; if (registrationToken) { headers['Authorization'] = `Bearer ${registrationToken}`; } const response = await ltiServiceFetch(registrationEndpoint, { method: 'POST', headers, body: JSON.stringify(registrationPayload), }); if (!response.ok) { const errorText = await response.json(); logger.error({ errorText }, 'lti dynamic registration error'); throw new Error(JSON.stringify(errorText)); } const data = await response.json(); logger.debug({ data }, 'Registration response'); const validated = RegistrationResponseSchema.parse(data); logger.debug({ validated }, 'Registration response validated'); return validated; }