Partilhar via


The Composer extension APIs

APPLIES TO: Composer v1.x and v2.x

It's possible to extend and customize the behavior of Composer at the application level by adding extensions. Extensions can hook into the internal mechanisms of Composer and change the way it operates. Extensions can also listen to activities inside Composer and react to them.

What's a Composer extension?

A Composer extension is a JavaScript module. When Composer starts, it loads extensions from its extensions subfolder. When a module is loaded, it has access to a set of Composer APIs, which it can use to provide functionality to the application.

Extensions don't have access to the entire Composer application. They're granted limited access to specific areas of the application, and must adhere to a set of interfaces and protocols.

Composer extension APIs

Extensions currently have access to the following functional areas:

  • Authentication and identity: extensions can provide a mechanism to gate access to the application and mechanisms used to provide user identity.
  • Storage: extensions can override the built-in filesystem storage with a new way to read, write and access bot projects.
  • Web server: extensions can add additional web routes to Composer's web server instance.
  • User interface: extensions can add custom user interfaces to different parts of the application
  • Publishing: extensions can add publishing mechanisms.
  • Runtime templates: extensions can provide a runtime template used when ejecting from Composer.

Combining these endpoints, it's possible to achieve scenarios such as:

  • Store content in a database.
  • Require login via Microsoft Entra ID or any other oauth provider.
  • Create a custom login screen.
  • Require login via GitHub, and use GitHub credentials to store content in a Git repo automatically.
  • Use Microsoft Entra ID roles to gate access to content.
  • Publish content to external services such as remote runtimes, content repositories, and testing systems.
  • Add custom authoring experiences inside Composer.

How to build an extension

Extension modules must come in one of the following forms:

  • Default export is a function that accepts the Composer extension API.
  • Default export is an object that includes an initialize function that accepts the Composer extension API.
  • A function called initialize is exported from the module.

Extensions can be loaded into Composer with the following method:

  • The extension is placed in the /extensions/ folder, and contains a package.json file with a configured composer definition.

The simplest form of an extension module is below:

export default async (composer: any): Promise<void> => {

  // call methods (see below) on the composer API
  // composer.useStorage(...);
  // composer.usePassportStrategy(...);
  // composer.addWebRoute(...)

}

Authentication and identity

To provide auth and identity services, Composer has in large part adopted PassportJS instead of implementing a custom solution. Extensions can use one of the many existing Passport strategies, or provide a custom strategy.

composer.usePassportStrategy(strategy)

Configure a Passport strategy to be used by Composer. This is the equivalent of calling app.use(passportStrategy) on an Express app. See PassportJS docs.

In addition to configuring the strategy, extensions will also need to use composer.addWebRoute to expose login, logout, and other related routes to the browser.

Calling this method also enables a basic auth middleware that is responsible for gating access to URLs and providing a simple user serializer/deserializer. Developers may choose to override these components using the methods below.

composer.useAuthMiddleware(middleware)

Provide a custom middleware for testing the authentication status of a user. This will override the built-in auth middleware that is enabled by default when calling usePassportStrategy().

Developers may choose to override this middleware for various reasons, such as:

  • Apply different access rules based on URL
  • Do something more than check req.isAuthenticated, such as validate or refresh tokens, make database calls, and provide telemetry.

composer.useUserSerializers(serialize, deserialize)

Provide custom serialize and deserialize functions for storing and retrieving the user profile and identity information in the Composer session.

By default, the entire user profile is serialized to JSON and stored in the session. If this isn't desirable, extensions should override these methods and provide alternate methods.

For example, the below code demonstrates storing only the user ID in the session during serialization, and the use of a database to load the full profile out of a database using that ID during deserialization.

const serializeUser = function(user, done) {
  done(null, user.id);
};

const deserializeUser = function(id, done) {
  User.findById(id, function(err, user) {
    done(err, user);
  });
};

composer.useUserSerializers(serializeUser, deserializeUser);

composer.addAllowedUrl(url)

Allow access to a URL, without requiring authentication. The url parameter can be an Express-style route with wildcards, such as /auth/:stuff or /auth(.*).

This is primarily for use with authentication-related URLs. While the /login route is allowed by default, any other URL involved in authentication needs to be added to the allowlist.

For example, when using OAuth, there's a secondary URL for receiving the auth callback. This has to be in the allowlist, otherwise access will be denied to the callback URL and it will fail.

// define a callback url
composer.addWebRoute('get','/oauth/callback', someFunction);

// Add the callback to the allow list.
composer.addAllowedUrl('/oauth/callback');

plugLoader.loginUri

This value is used by the built-in authentication middleware to redirect the user to the login page. By default, it's set to '/login' but it can be reset by changing this member value.

If you specify an alternate URI for the login page, you must use addAllowedUrl to add it to the list of allowed callback URLs.

pluginLoader.getUserFromRequest(req)

This is a static method on the PluginLoader class that extracts the user identity information provided by Passport. It's used for web route implementations to get user identity information and provide it to other components of Composer.

For example:

const RequestHandlerX = async (req, res) => {

  const user = await PluginLoader.getUserFromRequest(req);

  // ... do some stuff

};

Storage

By default, Composer reads and writes assets to the local filesystem. Extensions may override this behavior by providing a custom implementation of the IFileStorage interface. See interface definition here

Though this interface is modeled after a filesystem interaction, the implementation of these methods doesn't require using the filesystem, or a direct implementation of folder and path structure. However, the implementation must respect that structure and respond in the expected ways. For instance, the glob method must treat path patterns the same way the filesystem glob would.

composer.useStorage(customStorageClass)

Provide an iFileStorage-compatible class to Composer.

The constructor of the class will receive two parameters: a StorageConnection configuration, pulled from Composer's global configuration (currently data.json), and a user identity object, as provided by any configured authentication extension.

The current behavior of Composer is to instantiate a new instance of the storage accessor class each time it's used. As a result, caution must be taken not to undertake expensive operations each time. For example, if a database connection is required, the connection might be implemented as a static member of the class, inside the extension's init code and made accessible within the extension module's scope.

The user identity provided by a configured authentication extension can be used for purposes such as:

  • provide a personalized view of the content
  • gate access to content based on identity
  • create an audit log of changes

If an authentication extension isn't configured, or the user isn't logged in, the user identity will be undefined.

The class is expected to be in the form:

class CustomStorage implements IFileStorage {
  constructor(conn: StorageConnection, user?: UserIdentity) {
    // ...
  }

  // ...
}

Web server

Extensions can add routes and middleware to the Express instance.

These routes are responsible for providing all necessary dependent assets, such as browser JavaScript and CSS.

Custom routes aren't rendered inside the front-end React application, and currently have no access to that application. They're independent pages—though nothing prevents them from making calls to the Composer server APIs.

composer.addWebRoute(method, url, callbackOrMiddleware, callback)

This is equivalent to using app.get() or app.post(). A simple route definition receives three parameters—the method, URL and handler callback function.

If a route-specific middleware is necessary, it should be specified as the third parameter, making the handler callback function the fourth.

The signature for callbacks is (req, res) => {}.

The signature for middleware is (req, res, next) => {}.

For example:

// simple route
composer.addWebRoute('get', '/hello', (req, res) => {
  res.send('HELLO WORLD!');
});

// route with custom middleware
composer.addWebRoute('get', '/logout', (req, res, next) => {
    console.warn('user is logging out!');
    next();
  },(req, res) => {
    req.logout();
    res.redirect('/login');
});

composer.addWebMiddleware(middleware)

Bind an additional custom middleware to the web server. Middleware applied this way will be applied to all routes.

The signature for middleware is (req, res, next) => {}.

For middleware dealing with authentication, extensions must use useAuthMiddleware() as otherwise the built-in auth middleware will still be in place.

User interface

Extensions can host and serve custom UI in the form of a bundled React application at select entries called contribution points.

A detailed set of explanation, documentation, and examples are on GitHub.

Publishing

composer.addPublishMethod(publishMechanism, schema, instructions)

By default, the publish method will use the name and description from the package.json file. However, you may provide a customized name:

composer.addPublishMethod(publishMechanism, schema, instructions, customDisplayName, customDisplayDescription);

This provides a new mechanism by which a bot project is transferred from Composer to some external service. The mechanisms can use whichever method necessary to process and transmit the bot project to the desired external service, though it must use a standard signature for the methods.

In most cases, the extension itself does NOT include the configuration information required to communicate with the external service. Configuration is provided by the Composer application at invocation time.

Once registered as an available method, users can configure specific target instances of that method on a per-bot basis. For example, a user may install a Publish to PVA extension, which implements the necessary protocols for publishing to PVA. Then, in order to actually perform a publish, they would configure an instance of this mechanism, "Publish to HR Bot Production Slot", that includes the necessary configuration information.

Publishing extensions support the following features:

  • publish—given a bot project, publish it. Required.
  • getStatus—get the status of the most recent publish. Optional.
  • getHistory—get a list of historical publish actions. Optional.
  • rollback—roll back to a previous publish (as provided by getHistory). Optional.
publish(config, project, metadata, user)

This method is responsible for publishing the project using the provided config using whatever method the extension is implementing—for example, publish to Azure. This method is required for all publishing extensions.

To publish a project, this method must perform any necessary actions such as:

  • The LUIS lubuild process
  • Calling the appropriate runtime buildDeploy method
  • Doing the actual deploy operation

Note

Language Understanding (LUIS) will be retired on 1 October 2025. Beginning 1 April 2023, you won't be able to create new LUIS resources. A newer version of language understanding is now available as part of Azure AI Language.

Conversational language understanding (CLU), a feature of Azure AI Language, is the updated version of LUIS. For more information about question-and-answer support in Composer, see Natural language processing.

Parameters:

Parameter Description
config An object containing information from both the publishing profile and the bot's settings—see below.
project An object representing the bot project.
metadata Any comment passed by the user during publishing.
user A user object if one has been provided by an authentication extension.

Config will include:

{
  templatePath: '/path/to/runtime/code',
  fullSettings: {
    // all of the bot's settings from project.settings, but also including sensitive keys managed in-app.
    // this should be used instead of project.settings which may be incomplete
  },
  profileName: 'name of publishing profile',
  ... // All fields from the publishing profile
}

The project will include:

{
  id: 'bot id',
  dataDir: '/path/to/bot/project',
  files: // A map of files including the name, path and content
  settings: {
    // content of settings/appsettings.json
  }
}

Below is a simplified implementation of this process:

const publish = async(config, project, metadata, user) => {

  const { fullSettings, profileName } = config;

  // Prepare a copy of the project to build

  // Run the lubuild process

  // Run the runtime.buildDeploy process

  // Now do the final actual deploy somehow...

}
getStatus(config, project, user)

This method is used to check for the status of the most recent publish of project to a given publishing profile defined by the config field. This method is required for all publishing extensions.

This endpoint uses a subset of HTTP status codes to report the status of the deploy:

Status Meaning
200 Publish completed successfully
202 Publish is underway
404 No publish found
500 Publish failed

config will be in the form below. config.profileName can be used to identify the publishing profile being queried.

{
  profileName: `name of the publishing profile`,
  ... // all fields from the publishing profile
}

Should return an object in the form:

{
  status: [200|202|404|500],
  result: {
    message: 'Status message to be displayed in publishing UI',
    log: 'any log output from the process so far',
    comment: 'the user specified comment associated with the publish',
    endpointURL: 'URL to running bot for use with Emulator as appropriate',
    id: 'a unique identifier of this published version',
  }
}
getHistory(config, project, user)

This method is used to request a history of publish actions from a given project to a given publishing profile defined by the config field. This is an optional feature—publishing extensions may exclude this functionality if it's not supported.

config will be in the form below. config.profileName can be used to identify the publishing profile being queried.

{
  profileName: `name of the publishing profile`,
  ... // all fields from the publishing profile
}

Should return in array containing recent publish actions along with their status and log output.

[{
  status: [200|202|404|500],
  result: {
    message: 'Status message to be displayed in publishing UI',
    log: 'any log output from the process so far',
    comment: 'the user specified comment associated with the publish',
    id: 'a unique identifier of this published version',
  }
}]
rollback(config, project, rollbackToVersion, user)

This method is used to request a rollback in the deployed environment to a previously published version. This DOES NOT affect the local version of the project. This is an optional feature—publishing extensions may exclude this functionality if it's not supported.

config will be in the form below. config.profileName can be used to identify the publishing profile being queried.

{
  profileName: `name of the publishing profile`,
  ... // all fields from the publishing profile
}

rollbackToVersion will contain a version ID as found in the results from getHistory.

Rollback should respond using the same format as publish or getStatus and should result in a new publishing task:

{
  status: [200|202|404|500],
  result: {
    message: 'Status message to be displayed in publishing UI',
    log: 'any log output from the process so far',
    comment: 'the user specified comment associated with the publish',
    endpointURL: 'URL to running bot for use with Emulator as appropriate',
    id: 'a unique identifier of this published version',
  }
}

Runtime templates

composer.addRuntimeTemplate(templateInfo)

Expose a runtime template to the Composer UI. Registered templates will become available in the Runtime settings tab. When selected, the full content of the path will be copied into the project's runtime folder. Then, when a user selects Start Bot, the startCommand will be executed. The expected result is that a bot application launches and is made available to communicate with the Bot Framework Emulator.

await composer.addRuntimeTemplate({
  key: 'myUniqueKey',
  name: 'My Runtime',
  path: __dirname + '/path/to/runtime/template/code',
  startCommand: 'dotnet run',
  build: async(runtimePath, project) => {
    // implement necessary actions that must happen before project can be run
  },
  buildDeploy: async(runtimePath, project, settings, publishProfileName) => {
    // implement necessary actions that must happen before project can be deployed to azure

    return pathToBuildArtifacts;
  },
});
build(runtimePath, project)

Perform any necessary steps required before the runtime can be executed from inside Composer when a user selects the Start Bot button. Note this method shouldn't actually start the runtime directly—only perform the build steps.

For example, this would be used to call dotnet build in the runtime folder in order to build the application.

buildDeploy (runtimePath, project, settings, publishProfileName)
Parameter Description
runtimePath The path to the runtime that needs to be built.
project A bot project record.
settings A full set of settings to be used by the built runtime.
publishProfileName The name of the publishing profile that is the target of this build.

Perform any necessary steps required to prepare the runtime code to be deployed. This method should return a path to the build artifacts with the expectation that the publisher can perform a deploy of those artifacts as is and have them run successfully. To do this, it should:

  • Perform any necessary build steps
  • Install dependencies
  • Write settings to the appropriate location and format

composer.getRuntimeByProject(project)

Returns a reference to the appropriate runtime template based on the project's settings.

// load the appropriate runtime config
const runtime = composer.getRuntimeByProject(project);

// run the build step from the runtime, passing in the project as a parameter
await runtime.build(project.dataDir, project);

composer.getRuntime(type)

Get a runtime template by its key.

const dotnetRuntime = composer.getRuntime('csharp-azurewebapp');

Accessors

composer.passport

composer.name

Next steps