@saasquatch/integration-boilerplate-node
TypeScript icon, indicating that this package has built-in type declarations

2.1.2 • Public • Published

@saasquatch/integration-boilerplate-node

Node/Express boilerplate for building SaaSquatch integrations.

NPM version NPM downloads

SaaSquatch is a platform for managing referral and rewards programs for digital businesses. Integrations with SaaSquatch are typically built as microservices which respond to webhooks and form handler triggers from SaaSquatch's forms platform.

This boilerplate allows you to very quickly stand up a Node/Express microservice in TypeScript which can act as a SaaSquatch integration. It provides easy ways to do the easy things, and hooks for adding advanced configuration to make the more advanced things possible when building your integration.

Getting Started

Here's an example of the most basic integration that handles SaaSquatch webhooks:

import { createIntegrationService } from "@saasquatch/integration-boilerplate-node";

async function main() {
  const service = await createIntegrationService({
    handlers: {
      async webhookHandler(service, webhook, _config, _graphql, res) {
        service.logger.info("Handling a webhook! %o", webhook);
        res.sendStatus(200);
      },
    },
  });

  service.run();
}

main();

Concepts

Authentication

SaaSquatch integrations are authenticated in two primary ways:

  • As an OAuth application which provides access to SaaSquatch APIs from your integration. This requires an Auth0 application configured in SaaSquatch's backend for your integration.
  • With a tenant-scoped token provided to your integration's frontend on the Integrations page of the SaaSquatch portal and in the form configuration contexts for initial data and submit actions.

We are working on making it easier for customers to build their own integrations, however for now only SaaSquatch's integration team is able to properly configure the necessary resources for authenticating an integration.

Configuration

There are three different types of configuration relevant to an integration. They can be provided as type parameters to createIntegrationService in order to customize them for your integration:

createIntegrationService<ServiceConfig, IntegrationConfig, FormConfig>();

Service configuration

Service configuration parameters are configured with environment variables, which are typed through the use of the typed-config package. There are a number of built-in service configuration parameters common to all integrations. These are:

Environment Variable Property Default Description
SERVICE_NAME serviceName The name of the integration, in kebab case. For example: "my-cool-integration"
PORT port 10000 The port on which to run the microservice
SERVER_LOG_LEVEL serverLogLevel info The log level for the default logger
SERVER_LOG_FILE serverLogFile The optional path to write server logs to
ENFORCE_HTTPS enforceHttps true Enforce HTTPS on the Express server
PROXY_FRONTEND proxyFrontend Proxy the integration frontend through the Express server, specify a URL like http://localhost:3000
STATIC_FRONTEND_PATH staticFrontendPath .../../frontend/build The location (relative to your main module) of the integration's frontend (ignored if PROXY_FRONTEND is set)
STATIC_FRONTEND_INDEX staticFrontendIndex index.html The root file of your integration frontend (ignored if PROXY_FRONTEND is set)
SAASQUATCH_APP_DOMAIN saasquatchAppDomain app.referralsaasquatch.com The domain of the SaaSquatch core application
SAASQUATCH_AUTH0_DOMAIN saasquatchAuth0Domain The Auth0 domain for OAuth authentication to the SaaSquatch API
SAASQUATCH_AUTH0_CLIENT_ID saasquatchAuth0ClientId The Auth0 client ID for OAuth authentication to the SaaSquatch API
SAASQUATCH_AUTH0_SECRET saasquatchAuth0Secret The Auth0 client secret for OAuth authentication to the SaaSquatch API
GOOGLE_WORKLOAD_IDENTITY_PROJECT_ID googleWorkloadIdentityProjectId The project ID for Google Workload Identity Federation
GOOGLE_WORKLOAD_IDENTITY_POOL googleWorkloadIdentityPool The Google Workload Identity Federation pool
GOOGLE_WORKLOAD_IDENTITY_PROVIDER googleWorkloadIdentityProvider The Google Workload Identity Federation proivder
GOOGLE_WORKLOAD_IDENTITY_SERVICE_ACCOUNT_EMAIL googleWorkloadIdentityServiceAccountEmail The email address of the Google service account to request credentials for via Workload Identity Federation

The authentication parameters SAASQUATCH_AUTH0_* are required, and do not have defaults. Your service will not start without them.

Service configuration can be customized by extending BaseConfig using the primitives of typed-config:

import { key, optional } from "typed-config";
import {
  createIntegrationService,
  BaseConfig,
  asBoolean,
} from "@saasquatch/integration-boilerplate-node";

class MyCustomConfig extends BaseConfig {
  @key("REQUIRED_CUSTOM_SETTING")
  public requiredCustomSetting!: string;

  @key("OPTIONAL_CUSTOM_SETTING", asBoolean)
  @optional("default_value")
  public optionalCustomSetting!: asBoolean;
}

async function main() {
  const service = await createIntegrationService({
    configClass: MyCustomConfig,
  });

  // The config of the service is always available at service.config
  service.logger.debug(service.config.requiredCustomSetting);
}

main();

NOTE: At the the time of writing the asBoolean and asNumber transformers from typed-config were broken, so this package exports versions that work correctly. See this issue.

Integration configuration

Integration configuration is saved by your integration's configuration frontend and stored with the tenant in SaaSquatch. These parameters are the tenant's specific configuration for the integration, and are made available automatically to webhook and form handlers.

Form handler configuration

Form handler configuration is configuration specific to a particular form, and is configured by your integration's configuration frontend when called in a form's configuration context (either to configure initial data actions, or submit actions). The form handler configuration is available in the form handler context passed to a form handler function.

Handlers

A number of "on-rails" handler function definitions are available making simple integrations easy to develop.

Webhook handler

A webhook handler takes the following arguments:

Argument Description
service The integration service, which gives you access to service config, and the built-in logger
webhook The body of the webhook. SaaSquatch webhooks contain a type property to determine what kind of webhook it is
config The integration configuration for the tenant for which the webhook was sent
graphql A tenant-scoped GraphQL function for performing queries against SaaSquatch's GraphQL API
res The Express Response object

For webhooks to be considered successful by SaaSquatch, it must return a 200 HTTP status. If you define a webhook handler, you are responsible for sending the response status.

Webhook handlers do not need to return a body.

Form handlers

There are 4 types of form handler: formSubmitHandler, formValidateHandler, formIntrospectionHandler, formInitialDataHandler called at different times in a form's lifecycle. They all take the following arguments:

Argument Description
service The integration service, which gives you access to service config, and the built-in logger
context The form context, see the schema definition for the properties it contains
graphql A tenant-scoped GraphQL function for performing queries against SaaSquatch's GraphQL API

The expected response body of a form handler depends on the type, and the expected responses can be found in the schema:

Form handlers can also return an error response, which is defined in FormHandlerErrorResponseBody.

The Typescript types are based on these schemas and should help you make sure your handlers are returning the right kind of data.

Introspection handler

An integration introspection handler takes the following arguments:

Argument Description
service The integration service, which gives you access to service config, and the built-in logger
config The integration configuration for the tenant for which the webhook was sent
templateIntegrationConfig The system-wide integration config stored in CMS
tenantAlias The tenant alias of the tenant for which the introspection is taking place

For successful introspection, the integration should modify the templateIntegrationConfig as necessary and return it in the JSON body under the key templateIntegrationConfig.

Advanced Use Cases

Custom routing

Many integrations can get away with only providing handlers for webhooks and forms, however more complex integrations that may respond to webhooks from 3rd party systems need to define their own routing and middleware.

There is a router available on the service as service.router for this purpose, which can be configured with routes and middleware as needed.

import { createIntegrationService } from "@saasquatch/integration-boilerplate-node";

async function main() {
  const service = await createIntegrationService();

  service.router.get("/myCustomRoute", (req, res) => {
    res.send("Hello World");
  });

  service.run();
}

main();

NOTE: There are two reserved routes for the built-in handlers, /form and /webhook. Don't override these if you want them to work.

Custom routes called from Integrations page

Within the Integrations page in the SaaSquatch portal, your integration's configuration interface is provided with a tenant scoped token. For routes that are called by your configuration interface, there is a special middleware which will validate the token and place the tenantAlias directly on the request for you. This makes it easier to implement things like OAuth flows with 3rd party services or use your integration to securely query GraphQL for data required by your integration's frontend (like a list of programs, for example).

Integration configuration frontends are loaded in iframes and use Penpal for communication with the SaaSquatch portal. How to easily build an integration frontend is the concern of integration-boilerplate-react.

In your configuration interface you could do this:

fetch("/called-from-config-ui", {
  headers: { Authorization: `Bearer ${penpal.tenantScopedToken}` },
});

And respond to it in the integration like this (see the section on GraphQL below to understand the call to service.getTenant):

import { gql } from "@saasquatch/integration-boilerplate-node";

router.get(
  "/called-from-config-ui",
  service.tenantScopedTokenMiddleware, // This is the middleware you should use
  async (req, res) => {
    // The tenantAlias is on the req object
    const { graphql } = await service.getTenant(req.tenantAlias!);

    const response = await graphql(gql`query { ... }`);

    res.sendStatus(200);
  }
);

GraphQL

In your custom routes, you may need to make GraphQL queries based on data coming in from 3rd party services. The first thing to say is make sure you are appropriately authenticating incoming requests from 3rd parties!

To get the integration config for the tenant and a tenant-scoped GraphQL function, you can call getTenant on the service passing the tenant alias, which will return to you the integration config for the tenant and a tenant-scoped GraphQL function.

NOTE: If happen to pass a tenant alias for a tenant that does not have your integration enabled, then getTenant will fail.

Here's an example:

import { Router } from "express";
import {
  createIntegrationService,
  gql,
} from "@saasquatch/integration-boilerplate-node";

async function main() {
  const router = express.Router();
  const service = await createIntegrationService({
    handlers: {
      webhookHandler(service, webhook, config, graphql, res) {
        service.logger.info("Handling a webhook! %o", webhook);
        res.sendStatus(200);
      },
    },
    customRouter: router,
  });

  router.get("/some3rdPartyWebhook/:tenantAlias", async (req, res) => {
    // Ensure that the webhook is legitimate - implement something like this, or a middleware for this route
    // to validate the webhook is coming from the service you expect
    validate3rdPartyWebhook();

    // config - the tenant's integration config
    // graphql - a tenant-scoped GraphQL function
    const { config, graphql } = await service.getTenant(req.params.tenantAlias);

    const response = await graphql(gql`query { ... }`);

    res.sendStatus(200);
  });

  service.run();
}

main();

The graphql function can be typed with the response, i.e graphql<MyResponseType>(...) and takes optional variables and operationName arguments.

Google Workload Identity Federation

If your service requires access to Google services, typically you would generate a Service Account Key in JSON format, and place it within your environment as GOOGLE_APPLICATION_CREDENTIALS. Authorization is then controlled by the IAM permissions assigned to the service account.

Service account keys are powerful, and require regular rotation to prevent them from being compromised. An alternative to service account keys is Workload Identity Federation, which can bind an identity from an OpenID Connect Identity Provider (such as Auth0) to a service account in GCP.

As we already have Auth0 credentials for each service, it makes sense to use this method.

Configuring Workload Identity Federation is described in the Google documentation: https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers

In short, to successfully authorize access to Google services via Auth0 there are a number of configuration setups required:

  1. Configure a pool and provider for Workload Identity Federation
  2. Configure an API and machine-to-machine access for your Auth0 application
  3. Provide the Workload Identity configuration options to the service to automatically configure Google libraries

Configure Workload Identity Federation

Create a pool in Workload Identity Federation and add a new provider. The provider's Issuer URL should be your Auth0's tenant URL e.g. https://<tenant>.auth0.com/.

Under attribute mappings, you want to configure add at least the following:

  • google.subject -> assertion.sub
  • attribute.iss -> assertion.iss

To limit the identities allowed to just your Auth0 application, add an attribute condition that checks the issuer and the subject (replacing your Auth0 tenant domain and the client ID of your application):

assertion.iss == 'https://<tenant>.auth0.com/' && assertion.sub == '<clientId>@clients'

Then, grant access to your service account to be used by the identities in the pool.

Configure Auth0

In Auth0, we need to configure a new API so that we can generate tokens with the subject of the identity pool. Add a new API with a URL like https://iam.googleapis.com/projects/<projectNumber>/locations/global/workloadIdentityPools/<pool>/providers/<provider>.

Make sure to enable your Auth0 application for machine-to-machine access on this API.

Add service options

In your environment configure the following environment variables to automatically enable access to Google services via Workload Identity Federation:

  • GOOGLE_WORKLOAD_IDENTITY_PROJECT_ID
  • GOOGLE_WORKLOAD_IDENTITY_POOL
  • GOOGLE_WORKLOAD_IDENTITY_PROVIDER
  • GOOGLE_WORKLOAD_IDENTITY_SERVICE_ACCOUNT_EMAIL

Readme

Keywords

none

Package Sidebar

Install

npm i @saasquatch/integration-boilerplate-node

Weekly Downloads

0

Version

2.1.2

License

MIT

Unpacked Size

135 kB

Total Files

59

Last publish

Collaborators

  • 00salmon
  • locrian
  • jayden-chan
  • zachharrison
  • logvol
  • lisq
  • johanventer
  • dereksiemens
  • squatch-noahwc
  • ianhitchcock