@fluid-app/exigo-connection-sdk

中文文档
0.1.180发布于 2026/11/12UNLICENSEDTypeScriptES Module近 30 天下载 1.9万

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

原始使用文档
文档目录8 节
  1. Install
  2. 1. Credentials, once per app
  3. 2. REST
  4. Through Fluid's egress proxy (Mist apps)
  5. 3. SQL
  6. Through Fluid's egress proxy
  7. Errors
  8. Releasing

@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
ImportProvidesNeeds mssql
@fluid-app/exigo-connection-sdkdefineExigoCredentials, credential types, errorsno
@fluid-app/exigo-connection-sdk/restcreateExigoRestClient, errorsno
@fluid-app/exigo-connection-sdk/sqlgetExigoPool, closeExigoPool, errorsyes

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.

ErrorWhen
ExigoCredentialsErrora field is missing or invalid (fields names them)
ExigoAuthErrorREST 401/403, or a SQL login failure
ExigoTimeoutErrora REST call, SQL connect or SQL query ran too long
ExigoConnectionErrorDNS, refused connection, TLS, network
ExigoProxyErrorthe egress proxy refused the tunnel (status)
ExigoApiErrorREST 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.

版本与发布记录

@fluid-app/exigo-connection-sdk 共发布 181 个版本,首次发布于 2026/10/05;过去 12 个月发布了 181 个版本。

版本发布日期距今
0.1.180latest2026/11/123 天前
0.1.1792026/11/123 天前
0.1.1782026/11/0510 天前
0.1.1772026/11/0510 天前
0.1.1762026/11/0510 天前
0.1.1752026/11/0510 天前

关于 @fluid-app/exigo-connection-sdk 的常见问题

@fluid-app/exigo-connection-sdk 是什么?

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

如何安装 @fluid-app/exigo-connection-sdk?

在项目目录运行 npm install @fluid-app/exigo-connection-sdk,或使用 pnpm add / yarn add / bun add。

@fluid-app/exigo-connection-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@fluid-app/exigo-connection-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@fluid-app/exigo-connection-sdk 可以商用吗?

@fluid-app/exigo-connection-sdk 使用 UNLICENSED 许可。作者声明为 UNLICENSED,即不授予公开使用许可(与公共领域的 Unlicense 不同)。使用前需获得作者授权。以上为概括,不构成法律意见,请以许可原文为准。

@fluid-app/exigo-connection-sdk 的下载量有多少?

根据 npm 官方下载统计,@fluid-app/exigo-connection-sdk 在 2026/10/15 – 2026/11/13 这 30 天内被下载 19,237 次,日均约 641 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@fluid-app/exigo-connection-sdk 还在维护吗?

最新版本 0.1.180 发布于 2026/11/12(3 天前);至今共发布 181 个版本,其中过去 12 个月发布了 181 个;首次发布于 2026/10/05。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。