smallorange-gateway

Simple HTTP gateway for lambdas

Usage no npm install needed!

<script type="module">
  import smallorangeGateway from 'https://cdn.skypack.dev/smallorange-gateway';
</script>

README

CircleCI

Small Orange Gateway

Simple HTTP gateway for lambdas

This gateway takes care to create a HTTP server, call lambda functions, cache into Redis according to provided strategy and log into cloudWatch.

Sample

Setup

    // used env vars
    process.env.ACCESS_KEY_ID = 'xxxxx'; // (required)
    process.env.SECRET_ACCESS_KEY = 'xxxxx'; // (required)
    process.env.REGION = 'xxxxx'; // (optional)
    process.env.REDIS_URL = 'xxxxx'; // (optional)
    process.env.LOG_GROUP = 'xxxxx'; // (optional)
    process.env.PORT = 8080; // (optional)
    process.env.CACHE_PREFIX = ''; // (optional)
    process.env.CACHE_TTL = 2592000; // time in seconds to live (optional) default: 30 days
    process.env.CACHE_TTR = 7200; // time in seconds to refresh (optional) default: 2 hours
    process.env.CACHE_TIMEOUT = 1000; // time in ms to wait before route to the origin (optional) default: 1 second

    // lambdas manifest
    const lambdas = {
        '/': {
            name: 'functionName' // required,
            cache: {
                driver: {}, // Object like https://github.com/feliperohdee/smallorange-rxjs-cache-driver
                enabled: args => args.method === 'GET' && !args.hasExtension && !args.url.query || boolean,
                namespace: args => args.host, // required 
                key: args => args.url.pathname  || string // required,
                options: args => ({
                    ttl: number, // optional, override default
                    ttr: number // optional, override default
                })
            },
            transform: {
                args: args => { // it happens immediately after authentication is handled
                    args.headers = {
                        ...args.headers,
                        customHeader: 'customHeaderValue'
                    };
                },
                response: (response, req, res) => {
                    response.body = ...;
                    
                    return response;
                },
                error: err => {
                    return err;
                }
            }
        },
        // or with HTTP verb
        'POST /': {
            name: 'functionName' // required,
            cache: {
                enabled: args => args.method === 'GET' && !args.hasExtension && !args.url.query || boolean,
                namespace: args => args.host, // required 
                key: args => args.url.pathname  || string // required
            },
            transform: {
                args: args => {
                    args.headers = {
                        ...args.headers,
                        customHeader: 'customHeaderValue'
                    };
                }
            }
        },
        '/functionName': {
            name: 'functionName', // required
            // pass just params (not all args as described below) to the lambda function
            paramsOnly: true,
            defaults: {
                // default request params, it will be merged with params fetched from req.query, in case of key collision, the latter is going to have precedence
                requestParams: {
                    width: 100,
                    height: 100
                },
                // default response base64 value, lambda response can override this value, if checked, value will be converted to a buffer before returns to the browser
                responseBase64: true,
                // default response headers, lambda response headers will be merged with this value, in case of key collision, the latter is going to have precedence
                respondeHeaders: {
                    'content-type': 'image/png'
                }
            }
        },
        '/local': {
            name: 'functionName', // required,
            local: path.resolve('...path to local function index')	
        },
        '/mocked': {
            mocked: args => 'anything'
        },
        '/authOnly': {
            name: 'functionName' // required,
            auth: {
                enabled: true,
                allowedFields: ['role', 'user', 'loggedAt'], // (optional)
                handleInvalidSignature: false, // (optional)
                secret: (payload, params, headers) => 'mySecret' || 'mySecret', // (required)
                token(params, headers) => params.token || headers.authorization // (optional),
                options: {
                    /*
                    algorithms: List of strings with the names of the allowed algorithms. For instance, ["HS256", "HS384"].
                    audience: if you want to check audience (aud), provide a value here
                    issuer (optional): string or array of strings of valid values for the iss field.
                    ignoreExpiration: if true do not validate the expiration of the token.
                    ignoreNotBefore...
                    subject: if you want to check subject (sub), provide a value here
                    clockTolerance: number of seconds to tolerate when checking the nbf and exp claims, to deal with small clock differences among different servers
                    */
                },
                resolve: (auth, args) => object || Observable<Object> // (optional)
            }
        },
        '/adminOnly': {
            enabled: args => true,
            name: 'functionName' // required,
            auth: {
                // ...
                requiredRoles: ['admin']
            }
        },
        '/adminOrPublic': {
            name: 'functionName' // required,
            auth: {
                // ...
                requiredRoles: ['admin', 'public']
            }
        },
        
        // note: JWT should have role property, like:
        // {
        // 	role: string, // (required)
        // 	...anyOtherParams
        // }

        // full wildcards
        '/*': {
            name: 'functionName' // required,
        },
        '/*/*': {
            name: 'functionName' // required,
        },
        // partial wildcards
        '/*/functionName': {
            name: 'functionName' // required,
        },
        '/*/*/functionName': {
            name: 'functionName' // required,
        }
    };

    const gateway = new Gateway({
        logGroup: 'myAppLogs', // || env.LOG_GROUP
        lambdas,
        redisUrl: 'redis://localhost:6380', // || env.REDIS_URL
        cachePrefix: '', || // env.CACHE_PREFIX
    });

Usage Details

    // for a request like
    GET http://localhost/functionName/resource?string=value&number=2&boolean=true&nulled=null

    // lambda function will receive args like:
    {
        auth: {}, // if enabled
        body: {},
        hasExtension: false, // if url ends with .jpg or .png
        headers: {
            //...request headers
        },
        host: 'http://localhost',
        method: 'GET',
        // fetched from req.query
        params: {
            string: 'value',
            number: 2,
            boolean: true,
            nulled: null,
        },
        url: {
            path: '/functionName/resource?string=value&number=2&boolean=true&nulled=null',
            pathname: '/functionName/resource',
            query: 'string=value&number=2&boolean=true&nulled=null'
        },
        uri: '/functionName/resource'
    }

    // or just params if explicity declared at lambdas manifest with "paramsOnly = true":
    {
        string: 'value',
        number: 2,
        boolean: true,
        nulled: null
    }

    // lambdas can responds with just string, or an object with following signature
    {	
        //string or stringified object,
        body: string,
        headers: object,
        base64: boolean,
        statusCode: number // is statusCode >= 400, gateway is going to handle as an error following the Http/1.1 rfc (https://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
    }

Cache handling

    // you can manually mark cache to refresh making a request like:
    POST http://yourhost/cache
    {
        operation: 'markToRefresh',
        namespace: 'http://localhost'
    }

    // or unset
    POST http://yourhost/cache
    {
        operation: 'unset',
        namespace: 'http://localhost',
        keys: ['/', '/cart']
    }