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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@fluid-app/exigo-connection-sdk

v0.1.84

Published

Exigo REST client, SQL pool and credential interface for Fluid droplets and Mist apps

Readme

@fluid-app/exigo-connection-sdk

One way for Fluid droplets and Mist apps to talk to Exigo: a REST client, a cached SQL pool, a credential interface and typed errors. The package owns connection, auth and errors only. Business queries stay in each app.

Install

Public on npmjs:

pnpm add @fluid-app/exigo-connection-sdk
# Only for apps that query the SQL replica:
pnpm add mssql
pnpm add -D @types/mssql
# Only for apps that reach Exigo through Fluid's egress proxy:
pnpm add undici

| Import | Provides | Needs mssql | | -------------------------------------- | -------------------------------------------------- | ------------- | | @fluid-app/exigo-connection-sdk | defineExigoCredentials, credential types, errors | no | | @fluid-app/exigo-connection-sdk/rest | createExigoRestClient, errors | no | | @fluid-app/exigo-connection-sdk/sql | getExigoPool, closeExigoPool, errors | yes |

Runs on Node 20+: Cloud Run, and Vercel's Node runtime. Not on Edge.

1. Credentials, once per app

The package reads no environment variables and stores nothing. The app says where credentials come from; a missing field throws ExigoCredentialsError naming it.

// src/lib/exigo.ts — a multi-tenant droplet
import { defineExigoCredentials } from "@fluid-app/exigo-connection-sdk";

export const resolveExigoCredentials = defineExigoCredentials(
  async (companyId: string) => {
    const c = await prisma.credentials.findUniqueOrThrow({
      where: { companyId },
    });
    return {
      rest: {
        baseUrl: c.exigoApiBaseUrl,
        username: c.exigoApiUser,
        password: c.exigoApiPassword,
        company: c.exigoCompanyName,
      },
      sql: {
        host: c.exigoDbHost,
        database: c.exigoDbName,
        username: c.exigoDbUsername,
        password: c.exigoDbPassword,
        // Required, no default. true skips server certificate verification,
        // which Exigo's replicas need: they present a certificate that does
        // not verify against the host (e.g. SQL Server's self-signed one).
        trustServerCertificate: true,
      },
    };
  },
);

A single-tenant Mist app returns its env vars from the same function instead. The resolver runs on every call, so cache in the app if the lookup is costly.

2. REST

import {
  createExigoRestClient,
  ExigoAuthError,
  ExigoTimeoutError,
} from "@fluid-app/exigo-connection-sdk/rest";

const { rest } = await resolveExigoCredentials(company.id);
const exigo = createExigoRestClient(rest);

try {
  const account = await exigo.get<PointAccount>(
    "/customers/{id}/pointaccounts/{accountId}",
    { params: { id: customerId, accountId: pointAccountId } },
  );
} catch (error) {
  if (error instanceof ExigoAuthError) {
    /* wrong credentials: tell the admin */
  }
  if (error instanceof ExigoTimeoutError) {
    /* retry, or fail the callback fast */
  }
  throw error;
}

The client:

  • adds /3.0 to the base URL, and does not double it when the stored URL already has it; a base URL ending in another version, such as /2.0, throws ExigoCredentialsError rather than pinning the client to it;
  • requires an https base URL, since every request carries the password (http only for localhost, for a local stub);
  • sends Basic auth as username@company;
  • times out after 15 s by default (timeoutMs on the client or per call, at most 2,147,483,647 ms, the most setTimeout can hold);
  • resolves to the parsed JSON, or undefined for an empty body;
  • throws ExigoApiError for a non-2xx status, and for a 200 whose body is an HTML page or not JSON, rather than handing the page back as data; the password and Basic token are redacted from its message and bodySnippet;
  • refuses a path that is an absolute URL, so the credentials only ever go to the configured host, and a path that starts with an API version (/3.0/...), since the client adds it;
  • refuses a path param that is empty, . or .., which would otherwise resolve to a different endpoint than the path names.

get, delete take params, query, headers, timeoutMs and signal; post, put, patch also take a body, sent as JSON.

Through Fluid's egress proxy (Mist apps)

Exigo allowlists callers by source IP, and a Mist app on Vercel has no fixed outbound IP. Fluid's egress proxy (services/egress-proxy) lends it one: a CONNECT-only forward proxy whose static IP Exigo tenants allowlist. TLS stays end to end between the app and Exigo, so the proxy never sees the Exigo credentials.

pnpm add undici
const exigo = createExigoRestClient(rest, {
  egressProxy: process.env.FLUID_EGRESS_PROXY_URL
    ? {
        url: process.env.FLUID_EGRESS_PROXY_URL,
        // The install webhook's authentication_token (dit_…).
        token: installation.authenticationToken,
      }
    : undefined,
});
  • Fluid sets FLUID_EGRESS_PROXY_URL on a droplet-linked Mist app when its environment has a proxy. fluid mist env pull leaves it out on purpose: the proxy refuses callers off Fluid's platform, so local runs call Exigo directly, and the code above does that when the variable is unset.
  • The proxy authenticates the droplet installation token, and lets through only droplets and Exigo hosts on its own allowlists.
  • The URL must be https (http only for a loopback proxy run locally), since the CONNECT request carries the token outside the tunnel.
  • A refusal throws ExigoProxyError, a subclass of ExigoConnectionError whose status is the proxy's answer: 407 for a token it does not accept, 403 for a droplet or host it does not let through, 502 or 504 when it could not reach Exigo, 503 when it could not verify the token or is at capacity.
  • Clients built with the same proxy URL and token share one agent, so a client per request still reuses open tunnels.
  • SQL goes through the same proxy; see SQL through the egress proxy.

3. SQL

import { getExigoPool } from "@fluid-app/exigo-connection-sdk/sql";

const { sql: creds } = await resolveExigoCredentials(company.id);
const pool = await getExigoPool(creds);

const { recordset } = await pool
  .request()
  .input("email", email)
  .query("SELECT CustomerID FROM Customers WHERE Email = @email");

getExigoPool returns an mssql ConnectionPool, cached per host, port, database and username, and within that per password. Repeated calls reuse it, concurrent first calls share one connect, and a failed connect is not cached. A rotated password opens its own pool without closing the previous one, so requests in flight across a rotation keep working and a rollback connects at once; the previous pool is closed once nothing has asked for it in five minutes, and a database's current pool once nothing has asked for it in 30. Pools are small (4 connections, closed after 10 s idle) because every running instance opens its own. Pass { max, idleTimeoutMs, connectTimeoutMs, requestTimeoutMs } as the second argument to change that; options apply when the pool opens.

For typed parameters, import them from mssql itself (import sql from "mssql"; request.input("id", sql.Int, id)).

Connect failures arrive as ExigoAuthError, ExigoTimeoutError or ExigoConnectionError, with messages that say what to check. Errors from the app's own queries are left alone; pass one through translateExigoSqlError to map a timeout or a dropped connection the same way. A query error (RequestError) comes back unchanged unless its code is a transport one (ETIMEOUT, ECONNCLOSED, ESOCKET), so a bad table name is never reported as a network failure.

Close pools on shutdown:

import { closeAllExigoPools } from "@fluid-app/exigo-connection-sdk/sql";

process.once("SIGTERM", () => void closeAllExigoPools());

In Next, keep mssql out of the bundle and off Edge:

// next.config.ts
export default { serverExternalPackages: ["mssql"] };

// every route that queries
export const runtime = "nodejs";

Through Fluid's egress proxy

The same proxy as for REST, for the same reason: Exigo allowlists the replica by source IP. mssql ignores proxy settings, so the pool hands tedious a connector that opens a CONNECT tunnel to the replica's host and port; TDS, including its TLS handshake, then runs through the tunnel unchanged. No extra package is needed.

const pool = await getExigoPool(creds, {
  egressProxy: process.env.FLUID_EGRESS_PROXY_URL
    ? {
        url: process.env.FLUID_EGRESS_PROXY_URL,
        token: installation.authenticationToken, // dit_…
      }
    : undefined,
});
  • The proxy must allow the replica's host and port (1433), and Exigo must allowlist the proxy's IP on the SQL side.
  • The proxy and token are part of the pool's identity: a call with a different one gets its own pool.
  • Encryption is required through the proxy. encrypt: false is refused (ExigoCredentialsError). And because SQL Server negotiates TLS (tedious skips it, sending the login with the password merely obfuscated, when the server's PRELOGIN answer does not offer encryption), the connector reads that answer and hangs up before the login unless encryption is ON or REQ, throwing ExigoConnectionError. Direct connections are not checked this way.
  • Refusals throw ExigoProxyError with source: "sql"; a proxy that cannot be reached throws ExigoConnectionError naming it.
  • Known gap: the connection is encrypted, but with trustServerCertificate: true nothing proves the other end of the TLS is Exigo. The proxy reads nothing inside the tunnel, yet anything between the proxy and Exigo that impersonates the replica could read the login. Direct connections carry the same gap. Closing it needs a certificate on Exigo's side that can be verified.

Errors

Every error extends ExigoError. None of them carries a password or an Authorization header.

| Error | When | | ----------------------- | ----------------------------------------------------- | | ExigoCredentialsError | a field is missing or invalid (fields names them) | | ExigoAuthError | REST 401/403, or a SQL login failure | | ExigoTimeoutError | a REST call, SQL connect or SQL query ran too long | | ExigoConnectionError | DNS, refused connection, TLS, network | | ExigoProxyError | the egress proxy refused the tunnel (status) | | ExigoApiError | REST answered with an error or a body that isn't JSON |

source is "rest" or "sql" on the auth, timeout and connection errors.

Releasing

Listed in .github/workflows/frontend-publish-rep-packages.yml: a merge to main that changes this package publishes it to npm, bumping the patch version when the current one is already published.