@oomol-lab/connector
v1.0.0
Published
Core runtime client for the OOMOL Connector gateway. Runs fully without generated types (loose typing); install @oomol-lab/connector-types to light up precise per-action types.
Keywords
Readme
@oomol-lab/connector
English · 简体中文 · 繁體中文 · 日本語 · Русский · Français
Call any connector action in one typed line. Thin, zero-dependency HTTP client for the OOMOL Connector gateway — run actions, proxy upstream APIs, and introspect the catalog. Auth, OAuth, and credentials live on the gateway; the SDK is just the typed call.
Zero runtime dependencies. Works fully with loose typing — add @oomol-lab/connector-types to light up precise per-action types + JSDoc. No codegen, no CLI.
import { Connector } from "@oomol-lab/connector";
const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! }); // → Authorization: Bearer <apiKey>
// Path 1 — dynamic string, always callable
const { threads } = await oomol.execute("gmail.search_threads", { query: "from:boss" });
// Path 2 — namespace sugar, same call underneath
const r = await oomol.gmail.search_threads({ query: "from:boss" });[!TIP] Building for other people, or running the server yourself? This page covers the default
Connector— actions on your own connections. Two sibling clients ship in the same package:
- Connect accounts for your users —
ProjectConnector: your end-users link their Gmail / Slack / GitHub / … and you run actions on their behalf (the composio / pipedream managed-auth model).- Self-hosted runtime —
OpenConnector: the same typed calls, pointed at the open-source Connector server you host yourself.
Get an API key
You need an OOMOL Connector API key (shaped like api_…). Set it as OOMOL_API_KEY and you're ready — the SDK never validates the key locally; the gateway authorizes each request.
https://console.oomol.com/api-key
Install
npm install @oomol-lab/connector # or: bun add / pnpm add / yarn addRequires Node ≥ 18 (built-in fetch / AbortController). The runnable examples use Bun; the library itself is runtime-agnostic.
Concepts
No architecture to learn — just five words, because all the heavy lifting happens on the gateway:
- Gateway — the hosted OOMOL Connector service this client talks to. It holds credentials, performs the actual provider calls, and returns a uniform envelope. The SDK runs no integration logic locally; it only builds the request and parses the reply.
- Provider / service — a third-party API (
gmail,slack,github,notion, …). It's the<service>prefix of an action id. - Action — one operation on a provider, identified as
"<service>.<action>"(e.g.gmail.search_threads). You call actions; you don't define them — they live on the gateway. - Connection — a stored, already-authorized credential for a provider. You never touch tokens; you just name which connection to use via
connectionName. OAuth and credential lifecycle are the gateway's job, not the SDK's. - Team — optional tenant scoping.
What you can build
| Want to… | Use | Notes |
| --- | --- | --- |
| Run a modeled action | execute / executeRaw | The typed one-liner. executeRaw also returns { executionId, actionId, message }. |
| Hit an endpoint not yet modeled as an action | proxy | Passthrough to the upstream API, with the connection's credentials injected by the gateway. |
| Feed actions to an LLM / build dynamic forms | catalog | Runtime JSON Schema (2020-12) for any action or provider — catalog.action / catalog.actions / catalog.providers. |
| Discover what's connected | apps.list | Read-only list of the connections you've already linked. |
| Let your users connect their accounts | ProjectConnector | A separate project-scoped client to connect accounts on behalf of your end-users and run actions for them. See Connect accounts for your users. |
| Run the open-source server yourself | OpenConnector | Both call paths (execute and open.<service>.<action>) + catalog / apps / health against your self-hosted runtime. See Self-hosted runtime. |
Provider and action coverage comes from the gateway, not this package. Discover it at runtime with oomol.catalog.providers(), and see @oomol-lab/connector-types for the providers with precise compile-time types.
Precise types (optional)
The dynamic string path compiles for any actionId. Registered actions get literal completion + precise input/output; unregistered ones degrade to Record<string, any> instead of erroring — the SDK never blocks you when the types package lags the backend.
Install @oomol-lab/connector-types and add one side-effect import per provider you use:
import "@oomol-lab/connector-types/gmail"; // precise types + JSDoc for gmail.*
import "@oomol-lab/connector-types/slack"; // …and slack.*The core runtime never depends on the types package, so every action stays at least loosely callable.
[!NOTE] Requires
moduleResolutionset tobundler,node16, ornodenextso the subpath imports resolve. See@oomol-lab/connector-typesfor setup details.
Configuration
new Connector({
apiKey: process.env.OOMOL_API_KEY!, // required
baseUrl: "https://connector.oomol.com/v1", // default
team: "team-name", // → x-oo-team-name
connectionName: "work", // default connection (prefer per-call / using())
timeoutMs: 30_000, // default
maxRetries: 2, // default; retries 429 / 5xx / network with backoff
fetch: customFetch, // inject for tests / custom agents
});team— which tenant the call runs under.connectionName— which stored credential to use when you have more than one connection for a provider.
Per-call options (team, connectionName, signal, timeoutMs, retries) override a using() scope, which overrides client defaults:
const work = oomol.using({ connectionName: "work" }); // immutable scoped sub-client
await work.gmail.search_threads({ query }, { signal: controller.signal, timeoutMs: 10_000 });Error handling
import { ConnectorError, isRetryable } from "@oomol-lab/connector";
try {
await oomol.gmail.search_threads({ query });
} catch (err) {
if (err instanceof ConnectorError) {
err.code; // discriminable union, e.g. "rate_limited", "credential_expired"
err.status; // HTTP status (0 for client / network errors)
err.requestId; // also: err.actionId, err.executionId, err.data
if (isRetryable(err)) { /* retry */ }
}
}Recipes
Feedback to Notion in one call
Pushing user feedback into Notion normally means a Notion OAuth integration, their SDK, and hand-built block-payload JSON. Collapse all of that into one call — feedback arrives, you call append_block, it lands as a new paragraph at the bottom of your page.
import { Connector } from "@oomol-lab/connector";
import "@oomol-lab/connector-types/notion"; // optional — precise types + JSDoc on notion.*
const oomol = new Connector({ apiKey: process.env.OOMOL_API_KEY! });
const FEEDBACK_PAGE_ID = process.env.NOTION_FEEDBACK_PAGE_ID!;
Bun.serve({ routes: { "/feedback": async (req) => {
const { email, message } = await req.json();
await oomol.notion.append_block({ pageId: FEEDBACK_PAGE_ID, text: `${email ?? "anonymous"} — ${message}` });
return Response.json({ ok: true });
} } });The string path is identical — oomol.execute("notion.append_block", { pageId, text }). Full runnable version — examples/feedback-to-notion.ts.
Call an endpoint that has no action yet
When the gateway hasn't modeled an endpoint as an action, reach it directly with proxy — same auth, same connection, raw request/response.
const { status, data } = await oomol.proxy("github", {
endpoint: "/repos/oomol-lab/connector-sdk/issues",
method: "GET",
query: { state: "open" },
});Connect accounts for your users
Connector runs actions on your connections. ProjectConnector is the other half of the product, for building a SaaS platform on OOMOL: each of your end-users links their own Gmail / Slack / GitHub / … account through your app, and you run actions on their behalf — the composio / pipedream "managed auth" model.
It's a separate client, constructed with a project API key (oo_proj_…). It exposes only project-scoped operations — completely distinct from the personal Connector (different key, methods, and types), so there's nothing to mix up:
import { ProjectConnector } from "@oomol-lab/connector";
const project = new ProjectConnector({ apiKey: process.env.OOMOL_PROJECT_API_KEY! }); // oo_proj_...Identify each end-user with an opaque externalUserId you choose.
OAuth — create a link, then await completion
// Returns a pending connection request — send your user to `.authorizationUrl` to authorize.
const request = await project.connect.oauth("user_42", { service: "gmail", connectionName: "work" });
redirectUserTo(request.authorizationUrl);
// Poll until the user finishes (or it fails / expires); returns the final connection request.
const connected = await project.waitForConnection(request);API key / custom credential — synchronous, no waiting
const account = await project.connect.apiKey("user_42", { service: "openai", apiKey: "sk-..." });
await project.connect.customCredential("user_42", { service: "jira", values: { email, token } });Execute on the user's behalf
// The provider service is derived from the actionId prefix; the user's latest active account is used
// unless you pass `connectionName` (or `connectedAccountId`).
const out = await project.execute(
"user_42",
"gmail.search_threads",
{ query: "is:unread" },
{ connectionName: "work" },
);Scope to one user
const user = project.forUser("user_42"); // bind the end-user once; drop the repeated id
await user.connect.oauth({ service: "gmail" });
await user.execute("gmail.search_threads", { query: "from:ceo" });project.execute reuses the same @oomol-lab/connector-types registry as the core path — registered actions get precise input/output, the rest stay loosely callable.
[!NOTE]
connectionNameis the single name for a connection: you assign it onconnect.*, then pass it back asexecute'sconnectionNameto target that account (the gateway's wire field isalias). The end-user is alwaysexternalUserId. Connecting by API key or custom credential is synchronous — only OAuth needswaitForConnection.
Coming from composio / pipedream?
| composio / pipedream | @oomol-lab/connector |
| --- | --- |
| userId / external_user_id | externalUserId |
| connectedAccounts.initiate / createConnectToken (OAuth) | project.connect.oauth |
| connectedAccounts.initiate + AuthScheme.APIKey | project.connect.apiKey |
| waitForConnection() | project.waitForConnection() |
| tools.execute(slug, { userId, arguments }) | project.execute(externalUserId, actionId, input) |
| composio.getEntity(userId) | project.forUser(externalUserId) |
Full runnable lifecycle — examples/project.ts.
Self-hosted runtime
Running the open-source Connector server yourself (localhost, Docker, your own infra)? OpenConnector is the personal client for it — the same call surface you know (everything except using()), pointed at your own server:
import { OpenConnector } from "@oomol-lab/connector";
const open = new OpenConnector(); // defaults to http://localhost:3000; a fresh instance needs no auth
await open.execute("hackernews.get_top_stories", {}); // path 1 — dynamic string
await open.gmail.search_threads({ query: "from:boss" }); // path 2 — namespace sugar, same registry types
await open.proxy("github", { endpoint: "/user", method: "GET" }); // path 3 — passthrough (endpoint must be a relative path)
await open.catalog.search("send email", { limit: 5 }); // runtime extras: search, services, health
await open.apps.list();Auth is a single optional runtime token (oct_…), minted in the runtime's web console:
const open = new OpenConnector({
baseUrl: "https://connect.internal.example.com", // the server ORIGIN — not a /v1 url
runtimeToken: process.env.OOMOL_CONNECT_RUNTIME_TOKEN, // omit while the instance has no tokens
});[!NOTE] Connections, credentials, and OAuth setup are managed in the runtime's web console — that's server administration, deliberately outside this SDK. The client consumes what the console configured; connection selection has two layers (per-call
connectionNameover the client-level default — there is nousing()scope and noteam). And as on the hosted client, a service id that collides with a member name (execute/executeRaw/health/proxy/catalog/apps) keeps working throughexecute("<service>.<action>", …)— only its namespace sugar is shadowed.
Full runnable tour — examples/open.ts.
Why this SDK?
- Zero runtime dependencies —
sideEffects: false, ships onlydist. It's an in-process HTTP client, nothing more. - No codegen, no CLI — nothing to generate or run; install and call.
- Loose by default, precise on demand — every action is callable immediately; opt into per-action types one provider import at a time, and missing types never break your build.
- One uniform surface — every provider call, every error, and every retry follows the same shape.
Reference
oomol.proxy(service, { endpoint, method, query, headers, body })— passthrough to an upstream provider API (use it when no action models the endpoint yet).oomol.catalog.action / .actions / .providers— runtime JSON Schema for dynamic UIs, validation, or LLM tools.oomol.apps.list()— read-only introspection of your connected apps.oomol.executeRaw(...)— likeexecute, but returns{ data, executionId, actionId, message }.ProjectConnector— a separate client (project API key) to build a SaaS platform:connect.oauth/connect.apiKey/connect.customCredential,waitForConnection,execute/executeRawon a user's behalf, andforUserto scope to one user. See Connect accounts for your users.OpenConnector— the personal client for the open-source self-hosted runtime: both call paths (executeandopen.<service>.<action>),catalog/apps(+health,catalog.search/.services,apps.listByService/.authenticated), authenticated by an optional runtime token. See Self-hosted runtime.
See examples/ for runnable, type-checked usage of every method.
