@peerpage/gateway-node
v1.4.1
Published
Peerpage resident gateway for Node/TypeScript backends — an outbound WebSocket to the platform, a local allow-list dispatcher, and the same-origin identity-token route. Framework-agnostic core + thin Express adapter; one runtime dependency (ws).
Downloads
1,101
Maintainers
Readme
@peerpage/gateway-node
Trust model — read this first. This package runs inside your own Node/TypeScript backend. It opens an outbound WebSocket to the Peerpage platform and authenticates once with your tenant key; nothing executable ever crosses the socket — only invocation envelopes (a function name + params), which are matched against a local allow-list you defined in your own repo (the registrations you pass in). Unknown function → rejected. Bad params → rejected. The only SQL it ever issues is GRANT SELECT (never a data read or write) to the read-only login peerpage_api_read_only, derived solely from the committed peerpage/reads/*.json files in your repo — never from anything on the socket — and run through a DB connection you hand it (createDbConnection); omit that option and it touches no database at all. Its only inbound HTTP surface is the same-origin identity-token route. One runtime dependency (ws). It's a few hundred lines and meant to be read line by line — it is the trust surface a skeptical reviewer should audit. The TypeScript source ships in the package (node_modules/@peerpage/gateway-node/src), so you can read exactly what runs without leaving your own tree.
This is the Node/Express flavor of the Peerpage gateway. It is behaviorally identical to @peerpage/gateway-dotnet — both pass the same language-agnostic conformance suite in packages/protocol/conformance (that's what keeps flavors interchangeable).
Install
npm add @peerpage/gateway-node
# express is a peer dependency (you already have it)Wire it up (Express)
import { peerpageGateway } from "@peerpage/gateway-node/express";
import { registrations } from "./peerpage/index"; // THE MENU — your explicit allow-list
const { router, gateway } = peerpageGateway({
platformUrl: process.env.PEERPAGE_PLATFORM_URL!, // wss://hub.getpeerpage.com
tenantId: process.env.PEERPAGE_TENANT_ID!,
tenantKey: process.env.PEERPAGE_TENANT_KEY!, // from env — never hard-coded
registrations,
getUser: (req) => req.user && { id: req.user.id, roles: req.user.roles }, // your auth → Peerpage user
// Makes approved reads go LIVE on deploy: at boot the gateway reads peerpage/reads/*.json and
// applies the matching GRANT SELECT to peerpage_api_read_only, via a connection to YOUR database.
// Return a client that can GRANT — your app's own pool works. Omit it and approved reads keep
// showing sample data (Peerpage holds no DB connection of its own).
createDbConnection: async () => appPool.connect(),
});
app.use(router); // mounts the same-origin POST/GET /peerpage/token route
// on shutdown: await gateway.stop();createDbConnection returns anything with a query(sql, params) → { rows } method — pg.Pool, pg.Client, or a pooled client from pool.connect() (whichever of .release()/.end() it has is called afterwards, so a pooled client is returned to the pool, not closed). The gateway imports no database driver itself — you own it.
Framework-agnostic core (no Express) is also exported: new PeerpageGateway(options) + gateway.start()/stop(), and mintIdentityToken(...) if you mount the token route yourself.
Registrations — one file = one capability
// peerpage/registrations/orders/flagOrder.ts
import { defineFunction, p } from "@peerpage/gateway-node";
import { OrdersService } from "../../../services/orders.ts"; // call your OWN code, never reimplement
export const flagOrder = defineFunction({
name: "orders.flagOrder", // namespaced <domain>.<verb>
description: "Flag an order for review",
roles: ["admin", "superadmin"], // always explicit
params: p.object({ orderId: p.number({ int: true }), reason: p.string({ max: 500 }) }),
handle: (p, ctx) => new OrdersService().flagOrder(p.orderId, p.reason, ctx.user.id),
});params is the built-in dependency-free validator (p.object), but any zod-compatible { parse } works, so it fits your house style. peerpage/index.ts is the menu — an explicit array of every registration (no scanning).
Reads (the peerpage/reads/ folder). Each approved read is one peerpage/reads/<table>.json file — the folder IS the access-control list. When you wire createDbConnection, the gateway reconciles those files against the DB at boot: it GRANT SELECTs the listed columns to peerpage_api_read_only and revokes any column no longer listed (a deleted file revokes its whole table). Idempotent, derived only from the committed files, and it's the gateway's only DDL. If your app's DB principal can't GRANT, it logs the honest waiting state and a DBA applies the grants instead.
Deploying behind a reverse proxy
The gateway mounts /peerpage/token on your own backend, same origin as your app. If a reverse
proxy (nginx, Traefik, an ALB) fronts your app, add an explicit rule forwarding /peerpage/ to the
service that runs the gateway — otherwise the request falls through to your static/SPA handler and
returns 404/405, and the embed can never obtain a token. nginx:
location /peerpage/ {
proxy_pass http://your-api-upstream; # the service running the gateway
proxy_set_header Host $host;
proxy_set_header Authorization $http_authorization; # forward the caller's bearer token, if any
}Not for you if…
…your backend is .NET/C# — use @peerpage/gateway-dotnet. SQL Server behind a Node host isn't implemented yet (dialect: "postgres" is the supported engine); MSSQL customers run the .NET gateway.
