@politico/lambda
TypeScript icon, indicating that this package has built-in type declarations

1.0.8 • Public • Published

@politico/lambda

This library is meant to make it easier to spin up multi-use Lambda microservices integrated in an API Gateway. While it can be desireable to break Lambda functions down into their smallest possible forms, it's often more convenient to create Lambdas that do multiple things. For example, you might want to build a Lambda that reads from and writes to a database. Your project might even have several types of records and it's simplest to have one Lambda that handles CRUD operations for all of them. If you're used to something like Express, you may think about solving this problem by routing requests to different handler functions, but integrating Express in a Lambda environment can be a hassle. This library seeks to provide an Express-like routing experience while staying true to the simplicity of a Lambda environment.

Routing

In a simple case, setting up a Lambda router might look something like this:

import { createRouter, log, ok } from '@politico/lambda';

const router = createRouter({ log });

router.get('/hello/world', {
  handler: () => ok({
    response: {
      message: 'Hello world!',
    },
  }),
});

export const handler = (event) => router.handleLambdaApiGateway(event);

Here we're:

  1. creating a router that's set up to log requests to stdout,
  2. registering a route handler to respond to GET requests made to the /hello/world endpoint, and
  3. exporting a function called handler that can operate as the entrypoint for a Lambda function.

With that, you should be able to create your Lambda, connect it as an integration for an API Gateway, and ping your endpoint. Note that setting up logging is optional. If you don't pass in a log object then logging will be disabled, or you can pass in your own custom logger as long as it has a logLevel function that accepts a log level and additional arguments to log.

Adding Auth

This library supports token-based authentication out of the box, relying on an external service acting as the token authority. To configure your router with authentication, you can provide auth options like this:

import { createRouter, log } from '@politico/lambda';

const router = createRouter({
  logger: log,
  auth: {
    appName: 'my-app-name',
    secret: 'ssssshhhhh',
    signinUrl: 'https://auth.com/signin',
    roles: [
      'user',
      'admin',
    ],
  },
});

This then lets you configure routes with minimum roles set, like so:

router.get('/hello/world', {
  handler: myHandler,
  minimumRole: 'admin',
});

Now only users with the admin role can access this endpoint!

By how does the router know which roles a user has, or even who the user is? That's where the external token service comes in. The router will look for a user's token either as a bearer token in an Authorization header or as a newsroomAuthToken cookie. If it finds a token, it tries to parse it as a JWT using the provided secret. If the token is expired, it tries to refresh it by sending it to the token service at the provided signinUrl. If the token can be neither verified nor refreshed, authentication fails.

The router assumes that the payload of the token will contain information about the user as well as a set of permissions keyed by app name. The router will attempt to look up app-specific permissions using the provided appName as the key of the permissions object. A user may have multiple permissions (or "roles") for the same app.

Once a user's roles are established for the specified app, the router can check their authorization when attempting to access specific endpoints. This is where the router's roles configuration comes into play. This array of roles specifies two things: the set of roles supported by this app, and the relative "level" of each role. A user will be able to access an endpoint with a configured minimumRole if either (1) they have the exact specified role, or (2) they have at least one role of a strictly greater level than the level of the configured minimum role for that endpoint.

There are two ways you can specify the roles supported by a router. First, you can be fully explicit and specify each role's name and level:

import { createRouter, log } from '@politico/lambda';

const router = createRouter({
  logger: log,
  auth: {
    appName: 'my-app-name',
    secret: 'ssssshhhhh',
    signinUrl: 'https://auth.com/signin',
    roles: [
      { name: 'user-type-1', level: 0 },
      { name: 'user-type-2', level: 0 },
      { name: 'admin', level: 1 },
    ],
  },
});

This can be helpful for more complex role schemes. Notice that in the above example, both roles user-type-1 and user-type-2 have the level of 0 while admin has a level of 1. This means that if an endpoint has a minimum role of user-type-1, users with only the role user-type-2 won't be able to access it, and vice versa. Users with the admin role will be able to access both because the level of admin is greater than the levels of the user roles.

Often, an app's role scheme is more simply hierarchical and there are several increasing levels of access. As a convenience, you can specify roles as a simple array of names in this case:

import { createRouter, log } from '@politico/lambda';

const router = createRouter({
  logger: log,
  auth: {
    appName: 'my-app-name',
    secret: 'ssssshhhhh',
    signinUrl: 'https://auth.com/signin',
    roles: [
      'user',
      'admin',
    ],
  },
});

Here, the level of each role is assumed to be its index in the array of roles, so user has a level of 0 and admin has a role of 1.

Integrating with API Gateway

A common and convenient way to deploy Lambda microservices is as integrations behind an API Gateway that handles routing to multiple services. When using path-based routing within a Lambda microservice, this can create some complications, because whatever path prefix the API Gateway uses to route the request to the appropriate Lambda is included in the request event that gets passed to the Lambda.

The router supports removing a path prefix for its internal routing purposes automatically. To configure this, set the servicePathPrefix option:

import { createRouter, log, ok } from '@politico/lambda';

const router = createRouter({
  log,
  servicePathPrefix: '/service/my-service',
});

Now, any request that comes in will get /service/my-service stripped off of the start of the request path. Note that the router does not enforce that requests match this prefix; it simply removes the prefix if it's there.

Other Utilities

This library is built to make creating Lambda microservices easier, so it includes a few additional helpers.

  • loadEnvValue helps load values from the environment that will be type-safe as strings
  • ok and error help format Lambda handler responses
  • runDevServer runs an Express server that mimics an API Gateway integration for local development (note that this shouldn't be used in production)

Readme

Keywords

Package Sidebar

Install

npm i @politico/lambda

Weekly Downloads

0

Version

1.0.8

License

ISC

Unpacked Size

75.9 kB

Total Files

77

Last publish

Collaborators

  • dlazarenko-c-nitka
  • caitlinfloyd
  • tcrite_pol
  • ewalters-politico
  • andmilligan
  • rbvea
  • mshushan-politico
  • kherbert
  • pfriedr
  • arm5077
  • wickidd
  • stilessl
  • guirreri
  • mazet
  • brizandrew
  • bzjin