import SwaggerParser from '@apidevtools/swagger-parser'; // Copyright (c) Microsoft Corporation. /** * An enum that represents the types of errors that can occur during validation. */ var ErrorType; (function (ErrorType) { ErrorType["SpecNotValid"] = "spec-not-valid"; ErrorType["RemoteRefNotSupported"] = "remote-ref-not-supported"; ErrorType["NoServerInformation"] = "no-server-information"; ErrorType["UrlProtocolNotSupported"] = "url-protocol-not-supported"; ErrorType["RelativeServerUrlNotSupported"] = "relative-server-url-not-supported"; ErrorType["NoSupportedApi"] = "no-supported-api"; ErrorType["NoExtraAPICanBeAdded"] = "no-extra-api-can-be-added"; ErrorType["ResolveServerUrlFailed"] = "resolve-server-url-failed"; ErrorType["SwaggerNotSupported"] = "swagger-not-supported"; ErrorType["MultipleAuthNotSupported"] = "multiple-auth-not-supported"; ErrorType["SpecVersionNotSupported"] = "spec-version-not-supported"; ErrorType["ListFailed"] = "list-failed"; ErrorType["listSupportedAPIInfoFailed"] = "list-supported-api-info-failed"; ErrorType["FilterSpecFailed"] = "filter-spec-failed"; ErrorType["UpdateManifestFailed"] = "update-manifest-failed"; ErrorType["GenerateAdaptiveCardFailed"] = "generate-adaptive-card-failed"; ErrorType["GenerateFailed"] = "generate-failed"; ErrorType["ValidateFailed"] = "validate-failed"; ErrorType["GetSpecFailed"] = "get-spec-failed"; ErrorType["AuthTypeIsNotSupported"] = "auth-type-is-not-supported"; ErrorType["MissingOperationId"] = "missing-operation-id"; ErrorType["PostBodyContainMultipleMediaTypes"] = "post-body-contain-multiple-media-types"; ErrorType["ResponseContainMultipleMediaTypes"] = "response-contain-multiple-media-types"; ErrorType["ResponseJsonIsEmpty"] = "response-json-is-empty"; ErrorType["PostBodySchemaIsNotJson"] = "post-body-schema-is-not-json"; ErrorType["PostBodyContainsRequiredUnsupportedSchema"] = "post-body-contains-required-unsupported-schema"; ErrorType["ParamsContainRequiredUnsupportedSchema"] = "params-contain-required-unsupported-schema"; ErrorType["ParamsContainsNestedObject"] = "params-contains-nested-object"; ErrorType["RequestBodyContainsNestedObject"] = "request-body-contains-nested-object"; ErrorType["ExceededRequiredParamsLimit"] = "exceeded-required-params-limit"; ErrorType["NoParameter"] = "no-parameter"; ErrorType["NoAPIInfo"] = "no-api-info"; ErrorType["MethodNotAllowed"] = "method-not-allowed"; ErrorType["UrlPathNotExist"] = "url-path-not-exist"; ErrorType["Cancelled"] = "cancelled"; ErrorType["Unknown"] = "unknown"; })(ErrorType || (ErrorType = {})); /** * An enum that represents the types of warnings that can occur during validation. */ var WarningType; (function (WarningType) { WarningType["OperationIdMissing"] = "operationid-missing"; WarningType["GenerateCardFailed"] = "generate-card-failed"; WarningType["OperationOnlyContainsOptionalParam"] = "operation-only-contains-optional-param"; WarningType["ConvertSwaggerToOpenAPI"] = "convert-swagger-to-openapi"; WarningType["Unknown"] = "unknown"; })(WarningType || (WarningType = {})); /** * An enum that represents the validation status of an OpenAPI specification file. */ var ValidationStatus; (function (ValidationStatus) { ValidationStatus[ValidationStatus["Valid"] = 0] = "Valid"; ValidationStatus[ValidationStatus["Warning"] = 1] = "Warning"; ValidationStatus[ValidationStatus["Error"] = 2] = "Error"; })(ValidationStatus || (ValidationStatus = {})); var ProjectType; (function (ProjectType) { ProjectType[ProjectType["Copilot"] = 0] = "Copilot"; ProjectType[ProjectType["SME"] = 1] = "SME"; ProjectType[ProjectType["TeamsAi"] = 2] = "TeamsAi"; })(ProjectType || (ProjectType = {})); // Copyright (c) Microsoft Corporation. class SpecParserError extends Error { constructor(message, errorType) { super(message); this.errorType = errorType; } } // Copyright (c) Microsoft Corporation. class ConstantString { } ConstantString.CancelledMessage = "Operation is cancelled."; ConstantString.NoServerInformation = "No server information is found in the OpenAPI description document."; ConstantString.RemoteRefNotSupported = "Remote reference is not supported: %s."; ConstantString.MissingOperationId = "Missing operationIds: %s."; ConstantString.NoSupportedApi = "No supported API is found in the OpenAPI description document: only GET and POST methods are supported, additionally, there can be at most one required parameter, and no auth is allowed."; ConstantString.AdditionalPropertiesNotSupported = "'additionalProperties' is not supported, and will be ignored."; ConstantString.SchemaNotSupported = "'oneOf', 'allOf', 'anyOf', and 'not' schema are not supported: %s."; ConstantString.UnknownSchema = "Unknown schema: %s."; ConstantString.UrlProtocolNotSupported = "Server url is not correct: protocol %s is not supported, you should use https protocol instead."; ConstantString.RelativeServerUrlNotSupported = "Server url is not correct: relative server url is not supported."; ConstantString.ResolveServerUrlFailed = "Unable to resolve the server URL: please make sure that the environment variable %s is defined."; ConstantString.OperationOnlyContainsOptionalParam = "Operation %s contains multiple optional parameters. The first optional parameter is used for this command."; ConstantString.ConvertSwaggerToOpenAPI = "The Swagger 2.0 file has been converted to OpenAPI 3.0."; ConstantString.SwaggerNotSupported = "Swagger 2.0 is not supported. Please convert to OpenAPI 3.0 manually before proceeding."; ConstantString.SpecVersionNotSupported = "Unsupported OpenAPI version %s. Please use version 3.0.x."; ConstantString.MultipleAuthNotSupported = "Multiple authentication methods are unsupported. Ensure all selected APIs use identical authentication."; ConstantString.UnsupportedSchema = "Unsupported schema in %s %s: %s"; ConstantString.WrappedCardVersion = "devPreview"; ConstantString.WrappedCardSchema = "https://developer.microsoft.com/json-schemas/teams/vDevPreview/MicrosoftTeams.ResponseRenderingTemplate.schema.json"; ConstantString.WrappedCardResponseLayout = "list"; ConstantString.GetMethod = "get"; ConstantString.PostMethod = "post"; ConstantString.AdaptiveCardVersion = "1.5"; ConstantString.AdaptiveCardSchema = "http://adaptivecards.io/schemas/adaptive-card.json"; ConstantString.AdaptiveCardType = "AdaptiveCard"; ConstantString.TextBlockType = "TextBlock"; ConstantString.ImageType = "Image"; ConstantString.ContainerType = "Container"; ConstantString.RegistrationIdPostfix = { apiKey: "REGISTRATION_ID", oauth2: "CONFIGURATION_ID", http: "REGISTRATION_ID", openIdConnect: "REGISTRATION_ID", }; ConstantString.ResponseCodeFor20X = [ "200", "201", "202", "203", "204", "205", "206", "207", "208", "226", "default", ]; ConstantString.AllOperationMethods = [ "get", "post", "put", "delete", "patch", "head", "options", "trace", ]; // TODO: update after investigating the usage of these constants. ConstantString.WellknownResultNames = [ "result", "data", "items", "root", "matches", "queries", "list", "output", ]; ConstantString.WellknownTitleName = ["title", "name", "summary", "caption", "subject", "label"]; ConstantString.WellknownSubtitleName = [ "subtitle", "id", "uid", "description", "desc", "detail", ]; ConstantString.WellknownImageName = [ "image", "icon", "avatar", "picture", "photo", "logo", "pic", "thumbnail", "img", ]; ConstantString.ShortDescriptionMaxLens = 80; ConstantString.FullDescriptionMaxLens = 4000; ConstantString.CommandDescriptionMaxLens = 128; ConstantString.ParameterDescriptionMaxLens = 128; ConstantString.ConversationStarterMaxLens = 50; ConstantString.CommandTitleMaxLens = 32; ConstantString.ParameterTitleMaxLens = 32; ConstantString.SMERequiredParamsMaxNum = 5; ConstantString.DefaultPluginId = "plugin_1"; ConstantString.PluginManifestSchema = "https://aka.ms/json-schemas/copilot-extensions/v2.1/plugin.schema.json"; // Copyright (c) Microsoft Corporation. class Utils { static hasNestedObjectInSchema(schema) { if (schema.type === "object") { for (const property in schema.properties) { const nestedSchema = schema.properties[property]; if (nestedSchema.type === "object") { return true; } } } return false; } static containMultipleMediaTypes(bodyObject) { return Object.keys((bodyObject === null || bodyObject === void 0 ? void 0 : bodyObject.content) || {}).length > 1; } static isBearerTokenAuth(authScheme) { return authScheme.type === "http" && authScheme.scheme === "bearer"; } static isAPIKeyAuth(authScheme) { return authScheme.type === "apiKey"; } static isOAuthWithAuthCodeFlow(authScheme) { return !!(authScheme.type === "oauth2" && authScheme.flows && authScheme.flows.authorizationCode); } static getAuthArray(securities, spec) { var _a; const result = []; const securitySchemas = (_a = spec.components) === null || _a === void 0 ? void 0 : _a.securitySchemes; const securitiesArr = securities !== null && securities !== void 0 ? securities : spec.security; if (securitiesArr && securitySchemas) { for (let i = 0; i < securitiesArr.length; i++) { const security = securitiesArr[i]; const authArray = []; for (const name in security) { const auth = securitySchemas[name]; authArray.push({ authScheme: auth, name: name, }); } if (authArray.length > 0) { result.push(authArray); } } } result.sort((a, b) => a[0].name.localeCompare(b[0].name)); return result; } static getAuthInfo(spec) { let authInfo = undefined; for (const url in spec.paths) { for (const method in spec.paths[url]) { const operation = spec.paths[url][method]; const authArray = Utils.getAuthArray(operation.security, spec); if (authArray && authArray.length > 0) { const currentAuth = authArray[0][0]; if (!authInfo) { authInfo = authArray[0][0]; } else if (authInfo.name !== currentAuth.name) { throw new SpecParserError(ConstantString.MultipleAuthNotSupported, ErrorType.MultipleAuthNotSupported); } } } } return authInfo; } static updateFirstLetter(str) { return str.charAt(0).toUpperCase() + str.slice(1); } static getResponseJson(operationObject) { var _a, _b; let json = {}; let multipleMediaType = false; for (const code of ConstantString.ResponseCodeFor20X) { const responseObject = (_a = operationObject === null || operationObject === void 0 ? void 0 : operationObject.responses) === null || _a === void 0 ? void 0 : _a[code]; if ((_b = responseObject === null || responseObject === void 0 ? void 0 : responseObject.content) === null || _b === void 0 ? void 0 : _b["application/json"]) { multipleMediaType = false; json = responseObject.content["application/json"]; if (Utils.containMultipleMediaTypes(responseObject)) { multipleMediaType = true; json = {}; } else { break; } } } return { json, multipleMediaType }; } static convertPathToCamelCase(path) { const pathSegments = path.split(/[./{]/); const camelCaseSegments = pathSegments.map((segment) => { segment = segment.replace(/}/g, ""); return segment.charAt(0).toUpperCase() + segment.slice(1); }); const camelCasePath = camelCaseSegments.join(""); return camelCasePath; } static getUrlProtocol(urlString) { try { const url = new URL(urlString); return url.protocol; } catch (err) { return undefined; } } static resolveEnv(str) { const placeHolderReg = /\${{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*}}/g; let matches = placeHolderReg.exec(str); let newStr = str; while (matches != null) { const envVar = matches[1]; const envVal = process.env[envVar]; if (!envVal) { throw new Error(Utils.format(ConstantString.ResolveServerUrlFailed, envVar)); } else { newStr = newStr.replace(matches[0], envVal); } matches = placeHolderReg.exec(str); } return newStr; } static checkServerUrl(servers) { const errors = []; let serverUrl; try { serverUrl = Utils.resolveEnv(servers[0].url); } catch (err) { errors.push({ type: ErrorType.ResolveServerUrlFailed, content: err.message, data: servers, }); return errors; } const protocol = Utils.getUrlProtocol(serverUrl); if (!protocol) { // Relative server url is not supported errors.push({ type: ErrorType.RelativeServerUrlNotSupported, content: ConstantString.RelativeServerUrlNotSupported, data: servers, }); } else if (protocol !== "https:") { // Http server url is not supported const protocolString = protocol.slice(0, -1); errors.push({ type: ErrorType.UrlProtocolNotSupported, content: Utils.format(ConstantString.UrlProtocolNotSupported, protocol.slice(0, -1)), data: protocolString, }); } return errors; } static validateServer(spec, options) { var _a; const errors = []; let hasTopLevelServers = false; let hasPathLevelServers = false; let hasOperationLevelServers = false; if (spec.servers && spec.servers.length >= 1) { hasTopLevelServers = true; // for multiple server, we only use the first url const serverErrors = Utils.checkServerUrl(spec.servers); errors.push(...serverErrors); } const paths = spec.paths; for (const path in paths) { const methods = paths[path]; if ((methods === null || methods === void 0 ? void 0 : methods.servers) && methods.servers.length >= 1) { hasPathLevelServers = true; const serverErrors = Utils.checkServerUrl(methods.servers); errors.push(...serverErrors); } for (const method in methods) { const operationObject = methods[method]; if (((_a = options.allowMethods) === null || _a === void 0 ? void 0 : _a.includes(method)) && operationObject) { if ((operationObject === null || operationObject === void 0 ? void 0 : operationObject.servers) && operationObject.servers.length >= 1) { hasOperationLevelServers = true; const serverErrors = Utils.checkServerUrl(operationObject.servers); errors.push(...serverErrors); } } } } if (!hasTopLevelServers && !hasPathLevelServers && !hasOperationLevelServers) { errors.push({ type: ErrorType.NoServerInformation, content: ConstantString.NoServerInformation, }); } return errors; } static isWellKnownName(name, wellknownNameList) { for (let i = 0; i < wellknownNameList.length; i++) { name = name.replace(/_/g, "").replace(/-/g, ""); if (name.toLowerCase().includes(wellknownNameList[i])) { return true; } } return false; } static generateParametersFromSchema(schema, name, allowMultipleParameters, isRequired = false) { var _a, _b; const requiredParams = []; const optionalParams = []; if (schema.type === "string" || schema.type === "integer" || schema.type === "boolean" || schema.type === "number") { const parameter = { name: name, title: Utils.updateFirstLetter(name).slice(0, ConstantString.ParameterTitleMaxLens), description: ((_a = schema.description) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.ParameterDescriptionMaxLens), }; if (allowMultipleParameters) { Utils.updateParameterWithInputType(schema, parameter); } if (isRequired && schema.default === undefined) { parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); } } else if (schema.type === "object") { const { properties } = schema; for (const property in properties) { let isRequired = false; if (schema.required && ((_b = schema.required) === null || _b === void 0 ? void 0 : _b.indexOf(property)) >= 0) { isRequired = true; } const [requiredP, optionalP] = Utils.generateParametersFromSchema(properties[property], property, allowMultipleParameters, isRequired); requiredParams.push(...requiredP); optionalParams.push(...optionalP); } } return [requiredParams, optionalParams]; } static updateParameterWithInputType(schema, param) { if (schema.enum) { param.inputType = "choiceset"; param.choices = []; for (let i = 0; i < schema.enum.length; i++) { param.choices.push({ title: schema.enum[i], value: schema.enum[i], }); } } else if (schema.type === "string") { param.inputType = "text"; } else if (schema.type === "integer" || schema.type === "number") { param.inputType = "number"; } else if (schema.type === "boolean") { param.inputType = "toggle"; } if (schema.default) { param.value = schema.default; } } static parseApiInfo(operationItem, options) { var _a, _b; const requiredParams = []; const optionalParams = []; const paramObject = operationItem.parameters; if (paramObject) { paramObject.forEach((param) => { var _a; const parameter = { name: param.name, title: Utils.updateFirstLetter(param.name).slice(0, ConstantString.ParameterTitleMaxLens), description: ((_a = param.description) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.ParameterDescriptionMaxLens), }; const schema = param.schema; if (options.allowMultipleParameters && schema) { Utils.updateParameterWithInputType(schema, parameter); } if (param.in !== "header" && param.in !== "cookie") { if (param.required && (schema === null || schema === void 0 ? void 0 : schema.default) === undefined) { parameter.isRequired = true; requiredParams.push(parameter); } else { optionalParams.push(parameter); } } }); } if (operationItem.requestBody) { const requestBody = operationItem.requestBody; const requestJson = requestBody.content["application/json"]; if (Object.keys(requestJson).length !== 0) { const schema = requestJson.schema; const [requiredP, optionalP] = Utils.generateParametersFromSchema(schema, "requestBody", !!options.allowMultipleParameters, requestBody.required); requiredParams.push(...requiredP); optionalParams.push(...optionalP); } } const operationId = operationItem.operationId; const parameters = [...requiredParams, ...optionalParams]; const command = { context: ["compose"], type: "query", title: ((_a = operationItem.summary) !== null && _a !== void 0 ? _a : "").slice(0, ConstantString.CommandTitleMaxLens), id: operationId, parameters: parameters, description: ((_b = operationItem.description) !== null && _b !== void 0 ? _b : "").slice(0, ConstantString.CommandDescriptionMaxLens), }; return command; } static format(str, ...args) { let index = 0; return str.replace(/%s/g, () => { const arg = args[index++]; return arg !== undefined ? arg : ""; }); } static getSafeRegistrationIdEnvName(authName) { if (!authName) { return ""; } let safeRegistrationIdEnvName = authName.toUpperCase().replace(/[^A-Z0-9_]/g, "_"); if (!safeRegistrationIdEnvName.match(/^[A-Z]/)) { safeRegistrationIdEnvName = "PREFIX_" + safeRegistrationIdEnvName; } return safeRegistrationIdEnvName; } static getServerObject(spec, method, path) { const pathObj = spec.paths[path]; const operationObject = pathObj[method]; const rootServer = spec.servers && spec.servers[0]; const methodServer = spec.paths[path].servers && spec.paths[path].servers[0]; const operationServer = operationObject.servers && operationObject.servers[0]; const serverUrl = operationServer || methodServer || rootServer; return serverUrl; } } // Copyright (c) Microsoft Corporation. class Validator { listAPIs() { var _a; if (this.apiMap) { return this.apiMap; } const paths = this.spec.paths; const result = {}; for (const path in paths) { const methods = paths[path]; for (const method in methods) { const operationObject = methods[method]; if (((_a = this.options.allowMethods) === null || _a === void 0 ? void 0 : _a.includes(method)) && operationObject) { const validateResult = this.validateAPI(method, path); result[`${method.toUpperCase()} ${path}`] = { operation: operationObject, isValid: validateResult.isValid, reason: validateResult.reason, }; } } } this.apiMap = result; return result; } validateSpecVersion() { const result = { errors: [], warnings: [] }; if (this.spec.openapi >= "3.1.0") { result.errors.push({ type: ErrorType.SpecVersionNotSupported, content: Utils.format(ConstantString.SpecVersionNotSupported, this.spec.openapi), data: this.spec.openapi, }); } return result; } validateSpecServer() { const result = { errors: [], warnings: [] }; const serverErrors = Utils.validateServer(this.spec, this.options); result.errors.push(...serverErrors); return result; } validateSpecNoSupportAPI() { const result = { errors: [], warnings: [] }; const apiMap = this.listAPIs(); const validAPIs = Object.entries(apiMap).filter(([, value]) => value.isValid); if (validAPIs.length === 0) { const data = []; for (const key in apiMap) { const { reason } = apiMap[key]; const apiInvalidReason = { api: key, reason: reason }; data.push(apiInvalidReason); } result.errors.push({ type: ErrorType.NoSupportedApi, content: ConstantString.NoSupportedApi, data, }); } return result; } validateSpecOperationId() { const result = { errors: [], warnings: [] }; const apiMap = this.listAPIs(); // OperationId missing const apisMissingOperationId = []; for (const key in apiMap) { const { operation } = apiMap[key]; if (!operation.operationId) { apisMissingOperationId.push(key); } } if (apisMissingOperationId.length > 0) { result.warnings.push({ type: WarningType.OperationIdMissing, content: Utils.format(ConstantString.MissingOperationId, apisMissingOperationId.join(", ")), data: apisMissingOperationId, }); } return result; } validateMethodAndPath(method, path) { const result = { isValid: true, reason: [] }; if (this.options.allowMethods && !this.options.allowMethods.includes(method)) { result.isValid = false; result.reason.push(ErrorType.MethodNotAllowed); return result; } const pathObj = this.spec.paths[path]; if (!pathObj || !pathObj[method]) { result.isValid = false; result.reason.push(ErrorType.UrlPathNotExist); return result; } return result; } validateResponse(method, path) { const result = { isValid: true, reason: [] }; const operationObject = this.spec.paths[path][method]; const { json, multipleMediaType } = Utils.getResponseJson(operationObject); if (this.options.projectType === ProjectType.SME) { // only support response body only contains “application/json” content type if (multipleMediaType) { result.reason.push(ErrorType.ResponseContainMultipleMediaTypes); } else if (Object.keys(json).length === 0) { // response body should not be empty result.reason.push(ErrorType.ResponseJsonIsEmpty); } } return result; } validateServer(method, path) { const result = { isValid: true, reason: [] }; const serverObj = Utils.getServerObject(this.spec, method, path); if (!serverObj) { // should contain server URL result.reason.push(ErrorType.NoServerInformation); } else { // server url should be absolute url with https protocol const serverValidateResult = Utils.checkServerUrl([serverObj]); result.reason.push(...serverValidateResult.map((item) => item.type)); } return result; } validateAuth(method, path) { const pathObj = this.spec.paths[path]; const operationObject = pathObj[method]; const securities = operationObject.security; const authSchemeArray = Utils.getAuthArray(securities, this.spec); if (authSchemeArray.length === 0) { return { isValid: true, reason: [] }; } if (this.options.allowAPIKeyAuth || this.options.allowOauth2 || this.options.allowBearerTokenAuth) { // Currently we don't support multiple auth in one operation if (authSchemeArray.length > 0 && authSchemeArray.every((auths) => auths.length > 1)) { return { isValid: false, reason: [ErrorType.MultipleAuthNotSupported], }; } for (const auths of authSchemeArray) { if (auths.length === 1) { if ((this.options.allowAPIKeyAuth && Utils.isAPIKeyAuth(auths[0].authScheme)) || (this.options.allowOauth2 && Utils.isOAuthWithAuthCodeFlow(auths[0].authScheme)) || (this.options.allowBearerTokenAuth && Utils.isBearerTokenAuth(auths[0].authScheme))) { return { isValid: true, reason: [] }; } } } } return { isValid: false, reason: [ErrorType.AuthTypeIsNotSupported] }; } checkPostBodySchema(schema, isRequired = false) { var _a; const paramResult = { requiredNum: 0, optionalNum: 0, isValid: true, reason: [], }; if (Object.keys(schema).length === 0) { return paramResult; } const isRequiredWithoutDefault = isRequired && schema.default === undefined; const isCopilot = this.projectType === ProjectType.Copilot; if (isCopilot && this.hasNestedObjectInSchema(schema)) { paramResult.isValid = false; paramResult.reason = [ErrorType.RequestBodyContainsNestedObject]; return paramResult; } if (schema.type === "string" || schema.type === "integer" || schema.type === "boolean" || schema.type === "number") { if (isRequiredWithoutDefault) { paramResult.requiredNum = paramResult.requiredNum + 1; } else { paramResult.optionalNum = paramResult.optionalNum + 1; } } else if (schema.type === "object") { const { properties } = schema; for (const property in properties) { let isRequired = false; if (schema.required && ((_a = schema.required) === null || _a === void 0 ? void 0 : _a.indexOf(property)) >= 0) { isRequired = true; } const result = this.checkPostBodySchema(properties[property], isRequired); paramResult.requiredNum += result.requiredNum; paramResult.optionalNum += result.optionalNum; paramResult.isValid = paramResult.isValid && result.isValid; paramResult.reason.push(...result.reason); } } else { if (isRequiredWithoutDefault && !isCopilot) { paramResult.isValid = false; paramResult.reason.push(ErrorType.PostBodyContainsRequiredUnsupportedSchema); } } return paramResult; } checkParamSchema(paramObject) { const paramResult = { requiredNum: 0, optionalNum: 0, isValid: true, reason: [], }; if (!paramObject) { return paramResult; } const isCopilot = this.projectType === ProjectType.Copilot; for (let i = 0; i < paramObject.length; i++) { const param = paramObject[i]; const schema = param.schema; if (isCopilot && this.hasNestedObjectInSchema(schema)) { paramResult.isValid = false; paramResult.reason.push(ErrorType.ParamsContainsNestedObject); continue; } const isRequiredWithoutDefault = param.required && schema.default === undefined; if (isCopilot) { if (isRequiredWithoutDefault) { paramResult.requiredNum = paramResult.requiredNum + 1; } else { paramResult.optionalNum = paramResult.optionalNum + 1; } continue; } if (param.in === "header" || param.in === "cookie") { if (isRequiredWithoutDefault) { paramResult.isValid = false; paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); } continue; } if (schema.type !== "boolean" && schema.type !== "string" && schema.type !== "number" && schema.type !== "integer") { if (isRequiredWithoutDefault) { paramResult.isValid = false; paramResult.reason.push(ErrorType.ParamsContainRequiredUnsupportedSchema); } continue; } if (param.in === "query" || param.in === "path") { if (isRequiredWithoutDefault) { paramResult.requiredNum = paramResult.requiredNum + 1; } else { paramResult.optionalNum = paramResult.optionalNum + 1; } } } return paramResult; } hasNestedObjectInSchema(schema) { if (schema.type === "object") { for (const property in schema.properties) { const nestedSchema = schema.properties[property]; if (nestedSchema.type === "object") { return true; } } } return false; } } // Copyright (c) Microsoft Corporation. class CopilotValidator extends Validator { constructor(spec, options) { super(); this.projectType = ProjectType.Copilot; this.options = options; this.spec = spec; } validateSpec() { const result = { errors: [], warnings: [] }; // validate spec version let validationResult = this.validateSpecVersion(); result.errors.push(...validationResult.errors); // validate spec server validationResult = this.validateSpecServer(); result.errors.push(...validationResult.errors); // validate no supported API validationResult = this.validateSpecNoSupportAPI(); result.errors.push(...validationResult.errors); // validate operationId missing validationResult = this.validateSpecOperationId(); result.warnings.push(...validationResult.warnings); return result; } validateAPI(method, path) { const result = { isValid: true, reason: [] }; method = method.toLocaleLowerCase(); // validate method and path const methodAndPathResult = this.validateMethodAndPath(method, path); if (!methodAndPathResult.isValid) { return methodAndPathResult; } const operationObject = this.spec.paths[path][method]; // validate auth const authCheckResult = this.validateAuth(method, path); result.reason.push(...authCheckResult.reason); // validate operationId if (!this.options.allowMissingId && !operationObject.operationId) { result.reason.push(ErrorType.MissingOperationId); } // validate server const validateServerResult = this.validateServer(method, path); result.reason.push(...validateServerResult.reason); // validate response const validateResponseResult = this.validateResponse(method, path); result.reason.push(...validateResponseResult.reason); // validate requestBody const requestBody = operationObject.requestBody; const requestJsonBody = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content["application/json"]; if (requestJsonBody) { const requestBodySchema = requestJsonBody.schema; if (requestBodySchema.type !== "object") { result.reason.push(ErrorType.PostBodySchemaIsNotJson); } const requestBodyParamResult = this.checkPostBodySchema(requestBodySchema, requestBody.required); result.reason.push(...requestBodyParamResult.reason); } // validate parameters const paramObject = operationObject.parameters; const paramResult = this.checkParamSchema(paramObject); result.reason.push(...paramResult.reason); if (result.reason.length > 0) { result.isValid = false; } return result; } } // Copyright (c) Microsoft Corporation. class SMEValidator extends Validator { constructor(spec, options) { super(); this.projectType = ProjectType.SME; this.options = options; this.spec = spec; } validateSpec() { const result = { errors: [], warnings: [] }; // validate spec version let validationResult = this.validateSpecVersion(); result.errors.push(...validationResult.errors); // validate spec server validationResult = this.validateSpecServer(); result.errors.push(...validationResult.errors); // validate no supported API validationResult = this.validateSpecNoSupportAPI(); result.errors.push(...validationResult.errors); // validate operationId missing if (this.options.allowMissingId) { validationResult = this.validateSpecOperationId(); result.warnings.push(...validationResult.warnings); } return result; } validateAPI(method, path) { const result = { isValid: true, reason: [] }; method = method.toLocaleLowerCase(); // validate method and path const methodAndPathResult = this.validateMethodAndPath(method, path); if (!methodAndPathResult.isValid) { return methodAndPathResult; } const operationObject = this.spec.paths[path][method]; // validate auth const authCheckResult = this.validateAuth(method, path); result.reason.push(...authCheckResult.reason); // validate operationId if (!this.options.allowMissingId && !operationObject.operationId) { result.reason.push(ErrorType.MissingOperationId); } // validate server const validateServerResult = this.validateServer(method, path); result.reason.push(...validateServerResult.reason); // validate response const validateResponseResult = this.validateResponse(method, path); result.reason.push(...validateResponseResult.reason); let postBodyResult = { requiredNum: 0, optionalNum: 0, isValid: true, reason: [], }; // validate requestBody const requestBody = operationObject.requestBody; const requestJsonBody = requestBody === null || requestBody === void 0 ? void 0 : requestBody.content["application/json"]; if (Utils.containMultipleMediaTypes(requestBody)) { result.reason.push(ErrorType.PostBodyContainMultipleMediaTypes); } if (requestJsonBody) { const requestBodySchema = requestJsonBody.schema; postBodyResult = this.checkPostBodySchema(requestBodySchema, requestBody.required); result.reason.push(...postBodyResult.reason); } // validate parameters const paramObject = operationObject.parameters; const paramResult = this.checkParamSchema(paramObject); result.reason.push(...paramResult.reason); // validate total parameters count if (paramResult.isValid && postBodyResult.isValid) { const paramCountResult = this.validateParamCount(postBodyResult, paramResult); result.reason.push(...paramCountResult.reason); } if (result.reason.length > 0) { result.isValid = false; } return result; } validateParamCount(postBodyResult, paramResult) { const result = { isValid: true, reason: [] }; const totalRequiredParams = postBodyResult.requiredNum + paramResult.requiredNum; const totalParams = totalRequiredParams + postBodyResult.optionalNum + paramResult.optionalNum; if (totalRequiredParams > 1) { if (!this.options.allowMultipleParameters || totalRequiredParams > SMEValidator.SMERequiredParamsMaxNum) { result.reason.push(ErrorType.ExceededRequiredParamsLimit); } } else if (totalParams === 0) { result.reason.push(ErrorType.NoParameter); } return result; } } SMEValidator.SMERequiredParamsMaxNum = 5; // Copyright (c) Microsoft Corporation. class TeamsAIValidator extends Validator { constructor(spec, options) { super(); this.projectType = ProjectType.TeamsAi; this.options = options; this.spec = spec; } validateSpec() { const result = { errors: [], warnings: [] }; // validate spec server let validationResult = this.validateSpecServer(); result.errors.push(...validationResult.errors); // validate no supported API validationResult = this.validateSpecNoSupportAPI(); result.errors.push(...validationResult.errors); return result; } validateAPI(method, path) { const result = { isValid: true, reason: [] }; method = method.toLocaleLowerCase(); // validate method and path const methodAndPathResult = this.validateMethodAndPath(method, path); if (!methodAndPathResult.isValid) { return methodAndPathResult; } const operationObject = this.spec.paths[path][method]; // validate operationId if (!this.options.allowMissingId && !operationObject.operationId) { result.reason.push(ErrorType.MissingOperationId); } // validate server const validateServerResult = this.validateServer(method, path); result.reason.push(...validateServerResult.reason); if (result.reason.length > 0) { result.isValid = false; } return result; } } class ValidatorFactory { static create(spec, options) { var _a; const type = (_a = options.projectType) !== null && _a !== void 0 ? _a : ProjectType.SME; switch (type) { case ProjectType.SME: return new SMEValidator(spec, options); case ProjectType.Copilot: return new CopilotValidator(spec, options); case ProjectType.TeamsAi: return new TeamsAIValidator(spec, options); default: throw new Error(`Invalid project type: ${type}`); } } } // Copyright (c) Microsoft Corporation. /** * A class that parses an OpenAPI specification file and provides methods to validate, list, and generate artifacts. */ class SpecParser { /** * Creates a new instance of the SpecParser class. * @param pathOrDoc The path to the OpenAPI specification file or the OpenAPI specification object. * @param options The options for parsing the OpenAPI specification file. */ constructor(pathOrDoc, options) { this.defaultOptions = { allowMissingId: false, allowSwagger: false, allowAPIKeyAuth: false, allowMultipleParameters: false, allowBearerTokenAuth: false, allowOauth2: false, allowMethods: ["get", "post"], allowConversationStarters: false, allowResponseSemantics: false, allowConfirmation: false, projectType: ProjectType.SME, isGptPlugin: false, }; this.pathOrSpec = pathOrDoc; this.parser = new SwaggerParser(); this.options = Object.assign(Object.assign({}, this.defaultOptions), (options !== null && options !== void 0 ? options : {})); } /** * Validates the OpenAPI specification file and returns a validation result. * * @returns A validation result object that contains information about any errors or warnings in the specification file. */ async validate() { try { try { await this.loadSpec(); await this.parser.validate(this.spec, { validate: { schema: false, }, }); } catch (e) { return { status: ValidationStatus.Error, warnings: [], errors: [{ type: ErrorType.SpecNotValid, content: e.toString() }], }; } const errors = []; const warnings = []; if (!this.options.allowSwagger && this.isSwaggerFile) { return { status: ValidationStatus.Error, warnings: [], errors: [ { type: ErrorType.SwaggerNotSupported, content: ConstantString.SwaggerNotSupported, }, ], }; } // Remote reference not supported const refPaths = this.parser.$refs.paths(); // refPaths [0] is the current spec file path if (refPaths.length > 1) { errors.push({ type: ErrorType.RemoteRefNotSupported, content: Utils.format(ConstantString.RemoteRefNotSupported, refPaths.join(", ")), data: refPaths, }); } const validator = this.getValidator(this.spec); const validationResult = validator.validateSpec(); warnings.push(...validationResult.warnings); errors.push(...validationResult.errors); let status = ValidationStatus.Valid; if (warnings.length > 0 && errors.length === 0) { status = ValidationStatus.Warning; } else if (errors.length > 0) { status = ValidationStatus.Error; } return { status: status, warnings: warnings, errors: errors, }; } catch (err) { throw new SpecParserError(err.toString(), ErrorType.ValidateFailed); } } async listSupportedAPIInfo() { try { await this.loadSpec(); const apiMap = this.getAPIs(this.spec); const apiInfos = []; for (const key in apiMap) { const { operation, isValid } = apiMap[key]; if (!isValid) { continue; } const [method, path] = key.split(" "); const operationId = operation.operationId; // In Browser environment, this api is by default not support api without operationId if (!operationId) { continue; } const command = Utils.parseApiInfo(operation, this.options); const apiInfo = { method: method, path: path, title: command.title, id: operationId, parameters: command.parameters, description: command.description, }; apiInfos.push(apiInfo); } return apiInfos; } catch (err) { throw new SpecParserError(err.toString(), ErrorType.listSupportedAPIInfoFailed); } } /** * Lists all the OpenAPI operations in the specification file. * @returns A string array that represents the HTTP method and path of each operation, such as ['GET /pets/{petId}', 'GET /user/{userId}'] * according to copilot plugin spec, only list get and post method without auth */ // eslint-disable-next-line @typescript-eslint/require-await async list() { throw new Error("Method not implemented."); } /** * Generate specs according to the filters. * @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing. */ // eslint-disable-next-line @typescript-eslint/require-await async getFilteredSpecs(filter, signal) { throw new Error("Method not implemented."); } /** * Generates and update artifacts from the OpenAPI specification file. Generate Adaptive Cards, update Teams app manifest, and generate a new OpenAPI specification file. * @param manifestPath A file path of the Teams app manifest file to update. * @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing. * @param outputSpecPath File path of the new OpenAPI specification file to generate. If not specified or empty, no spec file will be generated. * @param pluginFilePath File path of the api plugin file to generate. */ // eslint-disable-next-line @typescript-eslint/require-await async generateForCopilot(manifestPath, filter, outputSpecPath, pluginFilePath, signal) { throw new Error("Method not implemented."); } /** * Generates and update artifacts from the OpenAPI specification file. Generate Adaptive Cards, update Teams app manifest, and generate a new OpenAPI specification file. * @param manifestPath A file path of the Teams app manifest file to update. * @param filter An array of strings that represent the filters to apply when generating the artifacts. If filter is empty, it would process nothing. * @param outputSpecPath File path of the new OpenAPI specification file to generate. If not specified or empty, no spec file will be generated. * @param adaptiveCardFolder Folder path where the Adaptive Card files will be generated. If not specified or empty, Adaptive Card files will not be generated. * @param isMe Boolean that indicates whether the project is an Messaging Extension. For Messaging Extension, composeExtensions will be added in Teams app manifest. */ // eslint-disable-next-line @typescript-eslint/require-await async generate(manifestPath, filter, outputSpecPath, adaptiveCardFolder, signal) { throw new Error("Method not implemented."); } async loadSpec() { if (!this.spec) { this.unResolveSpec = (await this.parser.parse(this.pathOrSpec)); if (!this.unResolveSpec.openapi && this.unResolveSpec.swagger === "2.0") { this.isSwaggerFile = true; } const clonedUnResolveSpec = JSON.parse(JSON.stringify(this.unResolveSpec)); this.spec = (await this.parser.dereference(clonedUnResolveSpec)); } } getAPIs(spec) { const validator = this.getValidator(spec); const apiMap = validator.listAPIs(); return apiMap; } getValidator(spec) { if (this.validator) { return this.validator; } const validator = ValidatorFactory.create(spec, this.options); this.validator = validator; return validator; } } // Copyright (c) Microsoft Corporation. class AdaptiveCardGenerator { static generateAdaptiveCard(operationItem) { try { const { json } = Utils.getResponseJson(operationItem); let cardBody = []; let schema = json.schema; let jsonPath = "$"; if (schema && Object.keys(schema).length > 0) { jsonPath = AdaptiveCardGenerator.getResponseJsonPathFromSchema(schema); if (jsonPath !== "$") { schema = schema.properties[jsonPath]; } cardBody = AdaptiveCardGenerator.generateCardFromResponse(schema, ""); } // if no schema, try to use example value if (cardBody.length === 0 && (json.examples || json.example)) { cardBody = [ { type: ConstantString.TextBlockType, text: "${jsonStringify($root)}", wrap: true, }, ]; } // if no example value, use default success response if (cardBody.length === 0) { cardBody = [ { type: ConstantString.TextBlockType, text: "success", wrap: true, }, ]; } const fullCard = { type: ConstantString.AdaptiveCardType, $schema: ConstantString.AdaptiveCardSchema, version: ConstantString.AdaptiveCardVersion, body: cardBody, }; return [fullCard, jsonPath]; } catch (err) { throw new SpecParserError(err.toString(), ErrorType.GenerateAdaptiveCardFailed); } } static generateCardFromResponse(schema, name, parentArrayName = "") { if (schema.type === "array") { // schema.items can be arbitrary object: schema { type: array, items: {} } if (Object.keys(schema.items).length === 0) { return [ { type: ConstantString.TextBlockType, text: name ? `${name}: \${jsonStringify(${name})}` : "result: ${jsonStringify($root)}", wrap: true, }, ]; } const obj = AdaptiveCardGenerator.generateCardFromResponse(schema.items, "", name); const template = { type: ConstantString.ContainerType, $data: name ? `\${${name}}` : "${$root}", items: Array(), }; template.items.push(...obj); return [template]; } // some schema may not contain type but contain properties if (schema.type === "object" || (!schema.type && schema.properties)) { const { properties } = schema; const result = []; for (const property in properties) { const obj = AdaptiveCardGenerator.generateCardFromResponse(properties[property], name ? `${name}.${property}` : property, parentArrayName); result.push(...obj); } if (schema.additionalProperties) { // TODO: better ways to handler warnings. console.warn(ConstantString.AdditionalPropertiesNotSupported); } return result; } if (schema.type === "string" || schema.type === "integer" || schema.type === "boolean" || schema.type === "number") { if (!AdaptiveCardGenerator.isImageUrlProperty(schema, name, parentArrayName)) { // string in root: "ddd" let text = "result: ${$root}"; if (name) { // object { id: "1" } text = `${name}: \${if(${name}, ${name}, 'N/A')}`; if (parentArrayName) { // object types inside array: { tags: ["id": 1, "name": "name"] } text = `${parentArrayName}.${text}`; } } else if (parentArrayName) { // string array: photoUrls: ["1", "2"] text = `${parentArrayName}: ` + "${$data}"; } return [ { type: ConstantString.TextBlockType, text, wrap: true, }, ]; } else { if (name) { return [ { type: "Image", url: `\${${name}}`, $when: `\${${name} != null}`, }, ]; } else { return [ { type: "Image", url: "${$data}", $when: "${$data != null}", }, ]; } } } if (schema.oneOf || schema.anyOf || schema.not || schema.allOf) { throw new Error(Utils.format(ConstantString.SchemaNotSupported, JSON.stringify(schema))); } throw new Error(Utils.format(ConstantString.UnknownSchema, JSON.stringify(schema))); } // Find the first array property in the response schema object with the well-known name static getResponseJsonPathFromSchema(schema) { if (schema.type === "object" || (!schema.type && schema.properties)) { const { properties } = schema; for (const property in properties) { const schema = properties[property]; if (schema.type === "array" && Utils.isWellKnownName(property, ConstantString.WellknownResultNames)) { return property; } } } return "$"; } static isImageUrlProperty(schema, name, parentArrayName) { const propertyName = name ? name : parentArrayName; return (!!propertyName && schema.type === "string" && Utils.isWellKnownName(propertyName, ConstantString.WellknownImageName) && (propertyName.toLocaleLowerCase().indexOf("url") >= 0 || schema.format === "uri")); } } export { AdaptiveCardGenerator, ConstantString, ErrorType, SpecParser, SpecParserError, Utils, ValidationStatus, WarningType }; //# sourceMappingURL=index.esm2017.js.map