npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@saasquatch/integration-boilerplate-node

v2.4.0

Published

Node/Express boilerplate for building SaaSquatch integrations

Downloads

83

Readme

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