Skip to main content

Plugins

Why plugins?

Cortex strives to make it easy to get a clear sense of what's happening in the world of development at every organization by centralizing the data from various tools and providing unique insights on top of them. But some organizations have unique use cases for tools, work with third parties that Cortex hasn’t integrated against yet, or strive to pull in data from internal systems that Cortex can’t know about or access. For these advanced use cases, writing a Cortex plugin is a solution for you!

Overview: how plugins work

Plugin code gets stored as a single HTML file, which is injected into an iframe inside Cortex. To access external APIs and bypass CORS restrictions, Cortex provides plugins access to plugin proxies. Plugin proxies can be configured to enhance requests to designated URL prefixes with headers, including rules that have access to secrets. Cortex also provides plugins basic contextual information about where the plugin is running inside of the app.

Creating a new plugin

Cortex makes it easy to spin up a new plugin repository using the Scaffolder tool. This template is already available for use in Cortex and spins up a new repository with a basic setup for each: React + TypeScript, linting and formatting (via eslint + prettier), testing (via testing-library), and compilation to a single HTML file (via webpack). You can check out the source yourself on GitHub. Additionally, it comes with our core plugin library (@cortexapps/plugin-core) pre-installed, which provides easy access to context, proxy usage, and UI components.

Registering a plugin

To register your plugin with Cortex, click Plugins in the main header and "Register plugin" to enter the plugin registration flow. You'll be asked to fill out some basic metadata about the plugin as well as upload the file containing your plug in HTML.

Developing a plugin

Cortex makes it easy to iterate on your plugin with a "dev mode" toggle on the create and edit plugin page and a "dev" script in the Scaffolded plugin repository. Simply run yarn dev in your plugin repository, then switch the "Dev mode" toggle on in the UI. When this setup is working, any code changes will be automatically re-built and hot reloaded into the plugin preview. Please note that for the plugin to be permanently updated, you must still run yarn build and upload the generated ui.html file before saving.

If your plugin was created before the dev script was available, see this PR for details on how to add the functionality to your plugin repo.

Accessing contextual Cortex information from your plugin

The easiest way to access the plugin context is via the usePluginContext() hook.

import { Stack, Title, usePluginContext } from "@cortexapps/plugin-core/components";
import type React from "react";

const MyComponent = React.FC => () => {
const context = usePluginContext();

return (
<Stack>
<Title level={2}>Plugin context</Title>
<pre>{JSON.stringify(context, null, 2)}</pre>
</Stack>
);
};

export default MyComponent;

If you need to access the plugin context outside of a React component, you can use the CortexApi class directly. The CortexAPI class exposed from @cortexapps/plugin-core provides a handy method for accessing the context your plugin is running in, getContext().

import { CortexApi } from "@cortexapps/plugin-core";
import types { CortexDomain, CortexResource, CortexService } from "@cortexapps/plugin-core";

// fetch information about the currently-signed-in Cortex user
const getCurrentCortexUser = async (): Promise<CortexUser> => {
const context = await CortexApi.getContext();
return context.user;
};

// if the plugin is running inside of a catalog entity details page, fetch information about that entity
const getCurrentCortexEntity = async (): Promise<CortexDomain | CortexResource | CortexService | undefined> => {
const context = await CortexApi.getContext();
return context.entity;
};

Accessing Cortex APIs from your plugin

Public Cortex APIs can be accessed without any special setup using @cortexapps/plugin-core’s CortexAPI. See the API docs for what information is available.

import { CortexApi } from "@cortexapps/plugin-core";

// fetch deploys for the current entity if the plugin is running on a domain, resource, or service details page
const fetchDeploysForCurrentEntity = async () => {
const context = await CortexApi.getContext();

if (!context?.entity) {
console.warn('Attempted to fetch deploys for an entity outside of the entity context');
return;
}

const { tag } = context.entity;

const response = await CortexApi.proxyFetch(`https://api.getcortexapp.com/api/v1/catalog/${tag}/deploys`);
return response.json();
};

Accessing external APIs from your plugin

Note

In this context, "external" means non-Cortex

Because plugins are run in an iframe, typical fetch requests often get blocked by the browser's enforcement of CORS. However, when using the Cortex-provided template, the browser fetch is shimmed to call CortexApi.proxyFetch, a method for using Cortex as a proxy to make the request. For this reason, you should be able to use fetch() as you typically would in a web application with less worry about the details.

If you find that your browser fetch is not getting shimmed properly, make sure that your @cortexapps/plugin-core is up to date and you're using wrapping your app with <PluginProvider>. See the cookiecutter template for an example.

Request signing

We add the following headers to each request made by Cortex:

  • x-cortex-timestamp (current timestamp in millis, used to prevent replay attacks)
  • x-cortex-signature
  • x-cortex-signature-256

These headers can be used to verify that the request is valid and originated from Cortex.

Note

x-cortex-signature contains the SHA1 hash of the request and exists for backward compatibility. SHA1 has been cracked and this signature should be considered deprecated. It is highly recommended to use x-cortex-signature-256, which contains the SHA256 hash.

To calculate the signature:

1. Create a string with the value "$timestamp.$requestBody" if the request body is non-null OR "$timestamp" if the request body is null. 
2. Calculate the SHA256 hash of this string using the Secret you provided to Cortex.
3. Verify that the x-cortex-signature-256 matches sha256=$calculatedSha256

Accessing authenticated APIs from your plugin

To access authenticated external APIs, you can configure a plugin proxy to add request headers to requests matching a URL prefix. Then, in your plugin configuration, indicate that it should use that particular proxy for requests coming from the plugin.

Example plugin proxy configuration

{
"id": "22",
"tag": "github",
"urlConfigurations": {
"https://api.github.com": {
"headers": [
{ "key": "X-GitHub-API-Version", "value": "2022-11-28" },
{ "key": "Authorization", "value": "Bearer {{ secrets.github_read_token }}" }
]
}
}
}

Example plugin code

import { CortexApi } from "@cortexapps/plugin-core";

const getGithubReleases = async (ownerName: string, repoName: string) => {
const githubReleases = await fetch(`http://api.github.com/repo/${ownerName}/${repoName}/releases`);
return githubReleases.json();
};

In this scenario, the request to fetch GitHub requests will be sent with the X-GitHub-API-Version and Authorization headers, with the Authorization header value interpolated to include the github_read_token secret if it exists in your Cortex workspace.

Using Cortex UI components

Cortex UI components are available for import from @cortexapps/plugin-core as of v1.1.0. You can view a Storybook of the components to see what's available, what props they take, and how they look.