@oomol-lab/connector
v1.2.1
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. The gateway owns the credential lifecycle —connectstarts an authorization, but the tokens, refresh, and storage stay server-side. - 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. |
| Connect an account of your own | connect | connect.oauth (async — authorization URL + poll), connect.apiKey / connect.customCredential (sync). Needs a team creator/admin key. See Connect your own account. |
| 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 your own account
Linking an account is normally a console job, but both personal clients can also do it in code. Three modes, two shapes of result:
// OAuth — ASYNCHRONOUS. Send the user to the URL, then poll.
const started = await oomol.connect.oauth("gmail", { returnUri: "https://app.example.com/done" });
console.log(started.authorizationUrl);
const settled = await oomol.connect.waitForConnection(started);
if (settled.status === "connected") console.log("connection id:", settled.appId);
// API key / custom credential — SYNCHRONOUS. The credential is validated and stored in one call.
const openai = await oomol.connect.apiKey("openai", { apiKey: process.env.OPENAI_API_KEY! });
const jira = await oomol.connect.customCredential("jira", {
values: { site: "acme.atlassian.net", email: "[email protected]", token: process.env.JIRA_TOKEN! },
});waitForConnection resolves on every terminal status — connected, failed (the user declined, or a newer attempt superseded this one), and expired (they never finished). It throws only when maxWaitMs runs out — yours, or the 10-minute default — with code client_wait_timeout. A retryable poll failure (429, 5xx, network) does not end the wait; it costs one poll and is retried on the next tick, inside that same cap. Prefer getAttempt(connectionRequestId) when you drive your own polling, or resume after a restart.
| Method | Returns | Notes |
| --- | --- | --- |
| connect.oauth(service, input?) | { authorizationUrl, stateHandle, connectionRequestId, status, expiresAt } | input: returnUri, authorizationOptionIds, extra, secretExtra — all optional. Poll with connectionRequestId, not stateHandle. |
| connect.apiKey(service, { apiKey, extra?, comment? }) | the ready connection | apiKey is the upstream provider's key, never your OOMOL key. |
| connect.customCredential(service, { values, comment? }) | the ready connection | values is keyed by the provider's declared credential fields. |
| connect.getAttempt(id) | one OAuth attempt | Readable until 24h past expiresAt; unknown ids reject with connection_request_not_found. |
| connect.waitForConnection(startOrId, opts?) | the settled attempt | pollIntervalMs (2s), maxWaitMs (10min, the authorization window). |
[!IMPORTANT] These are management calls, and the permission bar is higher than for running actions. On the hosted gateway the key's user must be
creatororadminof the effective team; a plain member is refused by the policy layer with a 403 before the request lands. On the self-hosted runtime the bar depends on how you configured it: a runtime with no authentication at all accepts these unauthenticated, but the moment runtime tokens are in play an admin token becomes mandatory — without one the runtime answers 403 (Configure an admin token to manage connections), and with one configured a runtime token (oct_…) is rejected with 401. See Self-hosted runtime.
Two things this surface deliberately does not do: name the connection (both backends assign the name themselves; rename it in the console afterwards) and delete or re-authorize one.
Full runnable tour — examples/connect.ts.
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 } });Who did they connect as?
// The third-party account holder behind a connected account — provider id, handle, display name,
// avatar, email (when the granted scopes expose it). Perfect for a "connected as …" UI.
const { service, profile, fetchedAt } = await project.getUserProfile(account.connectedAccountId);
console.log(`${service}: ${profile.displayName ?? profile.username}`);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" },
);Call a provider API through the project proxy
const profile = await project.proxy<{ emailAddress: string }>(
"user_42",
"gmail",
{ endpoint: "/users/me/profile", method: "GET" },
{ providerConfigId: "pc_1", connectedAccountId: "ca_1", retries: 0 },
);
if (profile.status >= 200 && profile.status < 300) {
console.log(profile.data.emailAddress);
}project.forUser("user_42").proxy("gmail", request, options) provides the same call with the user bound. Proxy uses the project's connected account and the provider's allowed endpoints and existing scopes. connectedAccountId takes precedence over connectionName; omit both only when latest-active account selection is intended.
The return value is { status, headers, data }, matching Connector.proxy. The upstream can return a non-2xx status inside a successful Connector response; inspect it explicitly. Connector failures throw ConnectorError. Set retries: 0 for requests that must not be repeated automatically. Use the HTTP envelope's meta.executionId when you need execution metadata; this method returns only the proxy output.
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
});connect works here too, with the same shape as on the hosted client — but connection management is admin-scoped on the runtime, so it takes a second token:
const open = new OpenConnector({
runtimeToken: process.env.OOMOL_CONNECT_RUNTIME_TOKEN, // runs actions, reads the catalog
adminToken: process.env.OOMOL_CONNECT_ADMIN_TOKEN, // required by `connect.*` only
});
const started = await open.connect.oauth("gmail", { returnUri: "http://localhost:5173/done" });
await open.connect.waitForConnection(started);The runtime rejects a runtime token on these routes, so the SDK never sends one there: connect.* carries adminToken, everything else carries runtimeToken. Omit adminToken and connect.* goes out unauthenticated — which is exactly right for a runtime that has no admin token configured.
[!NOTE] The rest of connection administration — deleting connections, OAuth client setup, minting tokens — stays in the runtime's web console, deliberately outside this SDK. 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/connect) 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.connect.oauth / .apiKey / .customCredential— link an account of your own, plusgetAttempt/waitForConnectionto follow an OAuth attempt to completion. Management-scoped: needs a teamcreator/adminkey (hosted) or the admin token (self-hosted). See Connect your own account.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,getUserProfileto read who the user is on the provider,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), andconnect, authenticated by an optional runtime token (connectadditionally requires the admin token on any runtime that enforces authentication). See Self-hosted runtime.
See examples/ for runnable, type-checked usage of every method.
