UNPKG

@fanoutio/serve-grip

Version:
391 lines (291 loc) 18.4 kB
## js-serve-grip GRIP library for JavaScript, provided as `express` and `hono` compatible middleware. This library is designed to assist the creation of backend server applications written in JavaScript that utilize [GRIP](https://pushpin.org/docs/protocols/grip/). This library is usable with the following frameworks: * [Express](https://expressjs.com/) * [Hono](https://hono.dev/) * [Next.js](https://nextjs.org/) * [connect](https://github.com/senchalabs/Connect) * [Koa](https://koajs.org/) *experimental support Supported GRIP servers include: * [Pushpin](http://pushpin.org/) * [Fastly Fanout](https://docs.fastly.com/products/fanout) Authors: Katsuyuki Omuro <komuro@fastly.com>, Konstantin Bokarius <kon@fanout.io> ## New for v3 ### Breaking changes - `ServeGripBase` no longer declares `monkeyPatchResMethodsForWebSocket` and `monkeyPatchResMethodsForGripInstruct` abstract methods. Instead, at the end of the `run` method, the `onAfterSetup()` method is called. ### Enhancements - Now adds support for [Hono](https://hono.dev/) framework. ## Usage ### Introduction [GRIP](https://pushpin.org/docs/protocols/grip/) is a protocol that enables a web service to delegate realtime push behavior to a proxy component, using HTTP and headers. `@fanoutio/serve-grip` is a server middleware that works with frameworks such as Express and Hono. It: * gives a simple and straightforward way to configure these frameworks against your GRIP proxy * parses the `Grip-Sig` header in any requests to detect if they came through a Grip proxy * provides your route handler with tools to handle such requests, such as: * access to information about whether the current request is proxied or is signed * methods you can call to issue any instructions to the GRIP proxy * provides access to the `Publisher` object, enabling your application to publish messages through the GRIP publisher. Additionally, `serve-grip` also handles [WebSocket-Over-HTTP processing](https://pushpin.org/docs/protocols/websocket-over-http/) so that WebSocket connections managed by the GRIP proxy can be controlled by your route handlers. ### Installation Install the library. ```sh npm install @fanoutio/serve-grip ``` #### Installation in Express / Connect Import the `ServeGrip` class from `@fanoutio/serve-grip/node` and instantiate the middleware. Then install it before your routes. Example: ```javascript import express from 'express'; import { ServeGrip } from '@fanoutio/serve-grip/node'; const app = express(); const serveGripMiddleware = new ServeGrip(/* config */); app.use(serveGripMiddleware); app.use('/path', (res, req) => { if (req.grip.isProxied) { const gripInstruct = res.grip.startInstruct(); gripInstruct.addChannel('test'); gripInstruct.setHoldStream(); res.end('[stream open]\n'); } }); app.listen(3000); ``` #### Installation in Hono > [!NOTE] > It's strongly recommended to use [TypeScript](https://www.typescriptlang.org) when working with Hono. Import the `serveGrip` function from `@fanoutio/serve-grip/hono` instantiate the middleware. Then install it before your routes. Specifying the configuration object for `serveGrip` can be done in a callback. This is useful for environments such as Fastly Compute, where configuration may not be available until request processing. Additionally, import the `Env` type and use it when instantiating `Hono`. This enables type checking for the `c.var.grip` context variable. Example: ```typescript import { serve } from '@hono/node-server'; import { Hono } from 'hono'; import { serveGrip, type Env } from '@fanoutio/serve-grip/hono'; const app = new Hono<Env>(); const serveGripMiddleware = serveGrip(() => /* config */); app.use(serveGripMiddleware); app.use( '/path', async (c) => { if (c.var.grip.isProxied) { const gripInstruct = c.var.grip.startInstruct(); gripInstruct.addChannel(CHANNEL_NAME); gripInstruct.setHoldStream(); return c.text('[stream open]\n'); } }); serve({ fetch: app.fetch, port: 3000 }, (addr) => { console.log(`Example app listening on port ${addr.port}!`) }); ``` > [!NOTE] > The above example is for Hono running on Node.js. Hono can be used with > other server platforms as well. For details on adapting the application to > other platforms, see [Hono's guide](https://hono.dev/docs/getting-started/basic). > > For examples of using this library with Hono on a backend application running > on Fastly Compute, check out the following example applications: > - [hono-compute-http](./examples/hono-compute-http) > - [hono-compute-ws](./examples/hono-compute-ws) #### Installation in Koa (experimental) Import the `ServeGrip` class from `@fanoutio/serve-grip/node` and instantiate it. The Koa middleware is available as the `.koa` property on the object. Install it before your routes. Example: ```javascript import Koa from 'koa'; import Router from '@koa/router'; import { ServeGrip } from '@fanoutio/serve-grip/node'; const app = new Koa(); const serveGripMiddleware = new ServeGrip(/* config */); app.use(serveGripMiddleware.koa); const router = new Router(); router.use( '/path', ctx => { if (ctx.req.grip.isProxied) { const gripInstruct = res.grip.startInstruct(); gripInstruct.addChannel('test'); gripInstruct.setHoldStream(); ctx.body = '[stream open]\n'; } }); app.use(router.routes()) .use(router.allowedMethods()); app.listen(3000); ``` #### Installation in Next.js You may use this library to add GRIP functionality to your [Next.js API Routes](https://nextjs.org/docs/api-routes/introduction). Import the `ServeGrip` class from `@fanoutio/serve-grip/node` and instantiate the middleware, and then run it in your handler before your application logic by calling the async function `serveGripMiddleware.run()`. Example: `/lib/grip.js`: ```javascript import { ServeGrip } from '@fanoutio/serve-grip/node'; export const serveGripMiddleware = new ServeGrip(/* config */); ``` `/pages/api/path.js`: ```javascript import { serveGripMiddleware } from '/lib/grip'; export default async(req, res) => { // Run the middleware if (!(await serveGripMiddleware.run(req, res))) { // If serveGripMiddleware.run() has returned false, it means the middleware has already // sent and ended the response, usually due to an error. return; } if (req.grip.isProxied) { const gripInstruct = res.grip.startInstruct(); gripInstruct.addChannel('test'); gripInstruct.setHoldStream(); res.end('[stream open]\n'); } } ``` > [!NOTE] > In Next.js, you must specifically call the middleware from each of your applicable API routes. > This is because in Next.js, your API routes will typically run on a serverless platform, and objects > will be recycled after each request. You are advised to construct a singleton instance of the > middleware in a shared location and reference it from your API routes. ### Configuration `@fanoutio/serve-grip/node` exports a class constructor named `ServeGrip`. This constructor takes a configuration object that can be used to configure the instance, such as the GRIP proxies to use for publishing or whether incoming requests should require a GRIP proxy. The following is an example of configuration when the GRIP proxy is an instance of Pushpin running on localhost: ```javascript import { ServeGrip } from '@fanoutio/serve-grip/node'; const serveGripMiddleware = new ServeGrip({ grip: { control_uri: 'https://localhost:5561/', // Control URI for Pushpin publisher control_iss: '<issuer>', // (opt.) iss needed for publishing, if required by Pushpin key: '<publish-key>', // (opt.) key needed for publishing, if required by Pushpin }, isGripProxyRequired: true, }); ``` The following is an example of configuration when the GRIP proxy is Fastly Fanout: ```javascript import { ServeGrip } from '@fanoutio/serve-grip/node'; const serveGripMiddleware = new ServeGrip({ grip: { control_uri: 'https://api.fastly.com/service/<service-id>/', // Control URI key: '<fastly-api-token>', // Authorization key for publishing (Fastly API Token) verify_iss: 'fastly:<service-id>', // Fastly issuer used for validating Grip-Sig verify_key: '<verify-key>', // Fastly public key used for validating Grip-Sig }, isGripProxyRequired: true, }); ``` Often the configuration is done using a `GRIP_URL` (and if needed, `GRIP_VERIFY_KEY`), allowing for configuration using simple strings. This allows for configuration from environment variables: ``` GRIP_URL="https://api.fastly.com/service/<service-id>/?verify-iss=fastly:<service-id>&key=<fastly-api-token>" GRIP_VERIFY_KEY="base64:LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZrd0V3WUhLb1pJemowQ0FRWUlLb1pJemowREFRY0RRZ0FFQ0tvNUExZWJ5RmNubVZWOFNFNU9uKzhHODFKeQpCalN2Y3J4NFZMZXRXQ2p1REFtcHBUbzN4TS96ejc2M0NPVENnSGZwLzZsUGRDeVlqanFjK0dNN3N3PT0KLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0t" ``` ```javascript import { ServeGrip } from '@fanoutio/serve-grip/node'; const serveGripMiddleware = new ServeGrip({ grip: process.env.GRIP_URL, gripVerifyKey: process.env.GRIP_VERIFY_KEY, isGripProxyRequired: true, }); ``` > [!NOTE] > When used with Hono, `@fanoutio/serve-grip/hono` exports a function named `serveGrip` rather than a constructor. > This function takes the same object as the parameter to the `ServeGrip` constructor described above, or a promise that > resolves to such an object. It can also be called with a callback that returns such an object or promise. > This is useful for environments such as Fastly Compute, where runtime configuration may not be available until > request processing. > > Example: > ```typescript > import { serveGrip } from '@fanoutio/serve-grip/hono'; > > const serveGripMiddleware = serveGrip(async () => { > const gripUrl = await loadGripUrl(); > return { > grip: gripUrl, > }; > }); > ``` Available options: | Key | Value | |---------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `grip` | A definition of GRIP proxies used to publish messages, or a preconfigured Publisher object from `@fanoutio/grip`. See below for details. | | `gripVerifyKey` | (optional) A string or Uint8Array that can be used to specify the `verify-key` component of the GRIP configuration.<br />Applies only if -<br />* `grip` is provided as a string, configuration object, or array of configuration objects<br />* `grip` does not already contain a `verify_key` value. | | `gripProxyRequired` | (optional) A boolean value representing whether all incoming requests should require that they be called behind a GRIP proxy. If this is true and a GRIP proxy is not detected, then a `501 Not Implemented` error will be issued. Defaults to `false`. | | `prefix` | (optional) A string that will be prepended to the name of channels being published to. This can be used for namespacing. Defaults to `''`. | In most cases your application will construct a singleton instance of this class and use it as the middleware. The `grip` parameter may be provided as any of the following: 1. An object with the following fields: | Field | Description | |---------------|-------------------------------------------------------------------------------------------------------| | `control_uri` | The Control URI of the GRIP client. | | `control_iss` | (optional) The Control ISS, if required by the GRIP client. | | `key` | (optional) string or Uint8Array. The key to use with the Control ISS, if required by the GRIP client. | | `verify_iss` | (optional) The ISS to use when validating a GRIP signature. | | `verify_key` | (optional) string or Uint8Array. The key to use when validating a GRIP signature. | 2. An array of such objects. 3. A GRIP URI, which is a string that encodes the above as a single string. 4. (advanced) A `Publisher` object that you have instantiated and configured yourself, from `@fanoutio/grip`. ### Handling a route After the middleware has run, your handler will receive the Grip context `grip` (On `req` and `res` objects or on the Hono context `c`). These provide access to the following: | Express, Koa, etc. | Hono | Description | |----------------------------|------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `req.grip.isProxied` | `c.var.grip.isProxied` | A boolean value indicating whether the current request has been called via a GRIP proxy. | | `req.grip.isSigned` | `c.var.grip.isSigned` | A boolean value indicating whether the current request is a signed request called via a GRIP proxy. | | `req.grip.wsContext` | `c.var.grip.wsContext` | If the current request has been made through WebSocket-Over-HTTP, then a `WebSocketContext` object for the current request. See `@fanoutio/grip` for details on `WebSocketContext`. | | `res.grip.startInstruct()` | `c.var.grip.startInstruct()` | Returns an instance of `GripInstruct`, which can be used to issue instructions to the GRIP proxy to hold connections. See `@fanoutio/grip` for details on `GripInstruct`. | To publish messages, obtain a `Publisher`. Use it to publish messages using the endpoints and prefix specified when the middleware was initialized. | Express, Koa, etc. | Hono | Description | |--------------------------------------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `serveGripMiddleware.getPublisher()` | `c.var.grip.getPublisher()` | Returns an instance of `Publisher`, which can be used to publish messages to the provided publishing endpoints using the provided prefix. See `@fanoutio/grip` for details on `Publisher`. | ### Examples This repository contains examples to illustrate the use of `serve-grip` in Connect / Express and Next.js, which can be found in the `examples` directory. For details on each example, please read the `README.md` files in the corresponding directories. ### Advanced #### Fastly Compute [Fastly Compute](https://www.fastly.com/documentation/guides/compute/getting-started-with-compute/) is an advanced edge computing platform that runs code (compiled to WebAssembly) on Fastly's global edge network. When using Fastly Compute, a single application can both **initiate a GRIP handoff** and **process proxied GRIP requests** by configuring the application itself as the backend. In this setup, the app runs *twice* per request: - once as the **outer Compute app**, which activates the GRIP proxy, and - once again as the **inner proxied app**, invoked through the GRIP proxy. To support this pattern, `@fanoutio/serve-grip/hono` provides an additional Hono middleware, **`fanoutSelfHandoffMiddleware`**, which is included automatically when running under the Fastly Compute JavaScript SDK (via the [`fastly` conditional runtime key](https://runtime-keys.proposal.wintercg.org/#fastly)). ```ts import { fanoutSelfHandoffMiddleware } from '@fanoutio/serve-grip/hono'; app.get('/api/*', fanoutSelfHandoffMiddleware('self')); ``` This middleware inspects whether the app is running in the outer Compute context or the inner proxied context: - In the **outer** context, it performs a Fanout handoff back to `'self'`, which must be defined as a backend in your Fastly service that points to itself. - In the **inner** context, it allows the request to continue as usual. > [!NOTE] > The examples in this repository demonstrate this configuration with > [Fastly Fanout local testing](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout/#run-the-service-locally): > - [hono-compute-http](./examples/hono-compute-http) > - [hono-compute-ws](./examples/hono-compute-ws) When deploying to your Fastly account, ensure that: 1. **Fanout is enabled** on your service, and 2. A **backend pointing to itself** is configured. For more details, see [Deploy to a Fastly Service](https://www.fastly.com/documentation/guides/concepts/real-time-messaging/fanout/#deploy-to-a-fastly-service) in the Fastly documentation. ## License (C) 2015, 2020 Fanout, Inc. (C) 2025 Fastly, Inc. Licensed under the MIT License, see file LICENSE.md for details.