@igniter-js/connectors
v0.1.11
Published
Type-safe, multi-tenant connector management library for Igniter.js with OAuth, encryption, and adapter support
Maintainers
Readme
@igniter-js/connectors
Type-safe connector management for multi-tenant integrations
Build OAuth, API-key, and webhook connectors with strong typing, encryption, and telemetry.
Quick Start • Core Concepts • Examples • API Reference • Telemetry
✨ Why @igniter-js/connectors?
Integrations are hard because they span auth, storage, webhooks, and observability. This package gives you a single, consistent model that scales across tenants and providers:
- ✅ Type-safe connectors with
StandardSchemaV1(Zod-compatible) - ✅ Multi-tenant scopes (organization, user, system, or custom)
- ✅ OAuth 2.0 with PKCE and refresh handling
- ✅ AES-256-GCM encryption for sensitive fields
- ✅ Webhook pipelines with validation and verification
- ✅ Adapter system (Prisma + Mock + custom)
- ✅ Telemetry-ready with structured events
- ✅ Predictable errors with stable error codes
🚀 Quick Start
Installation
# npm
npm install @igniter-js/connectors @igniter-js/common zod
# pnpm
pnpm add @igniter-js/connectors @igniter-js/common zod
# yarn
yarn add @igniter-js/connectors @igniter-js/common zod
# bun
bun add @igniter-js/connectors @igniter-js/common zod60-Second Setup
import { IgniterConnector, IgniterConnectorManager } from "@igniter-js/connectors";
import { IgniterConnectorPrismaAdapter } from "@igniter-js/connectors/adapters";
import { z } from "zod";
// 1) Define a connector
const telegram = IgniterConnector.create()
.withConfig(
z.object({
botToken: z.string(),
chatId: z.string(),
}),
)
.addAction("sendMessage", {
input: z.object({ message: z.string() }),
output: z.object({ messageId: z.string() }),
handler: async ({ input, config }) => {
const response = await fetch(
`https://api.telegram.org/bot${config.botToken}/sendMessage`,
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ chat_id: config.chatId, text: input.message }),
},
);
const data = await response.json();
return { messageId: String(data.result.message_id) };
},
})
.build();
// 2) Create the manager
const connectors = IgniterConnectorManager.create()
.withDatabase(IgniterConnectorPrismaAdapter.create(prisma))
.withEncrypt(["botToken"]) // Encrypt sensitive fields at rest
.addScope("organization", { required: true })
.addConnector("telegram", telegram)
.build();
// 3) Use scoped connectors
const scoped = connectors.scope("organization", "org_123");
await scoped.connect("telegram", { botToken: "token", chatId: "123" });
const { data, error } = await scoped.action("telegram", "sendMessage").call({
message: "Hello from Igniter.js",
});
if (error) throw error;
console.log("Message ID:", data?.messageId);✅ Success! You just created a multi-tenant connector with encryption and typed actions.
🎯 Core Concepts
Architecture Overview
┌─────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────┤
│ scoped.action('telegram','sendMessage').call(...) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ IgniterConnectorManagerCore │
│ - Scopes │
│ - Connectors │
│ - Hooks │
│ - Telemetry │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ PrismaAdapter | MockAdapter | CustomAdapter │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Storage Backend │
│ Postgres | MySQL | SQLite | Memory | Anything │
└─────────────────────────────────────────────────────────┘Mental Model
- Connector Definition (
IgniterConnector.create()) defines config, actions, OAuth, webhooks. - Manager Builder (
IgniterConnectorManager.create()) wires adapters, encryption, telemetry, scopes. - Scoped Instance (
connectors.scope(...)) performs operations per tenant. - Adapter persists connector configs (Prisma + mock + custom).
- Telemetry uses
IgniterConnectorsTelemetryEventsto emit consistent events.
🧭 Exports & Subpaths
Main Exports
import {
IgniterConnector,
IgniterConnectorManager,
IgniterConnectorManagerBuilder,
IgniterConnectorError,
IGNITER_CONNECTOR_ERROR_CODES,
IgniterConnectorCrypto,
IgniterConnectorFields,
IgniterConnectorSchema,
IgniterConnectorOAuthUtils,
IgniterConnectorUrl,
$Infer,
$InferScoped,
$InferConnectorKey,
$InferScopeKey,
$InferConfig,
$InferActionKeys,
} from "@igniter-js/connectors";Adapters Subpath
import {
IgniterConnectorBaseAdapter,
IgniterConnectorPrismaAdapter,
IgniterConnectorMockAdapter,
} from "@igniter-js/connectors/adapters";Telemetry Subpath
import { IgniterConnectorsTelemetryEvents } from "@igniter-js/connectors/telemetry";⚙️ Configuration
Environment Variables
| Variable | Required | Description |
| --- | --- | --- |
| IGNITER_SECRET | Yes (if using default encryption) | AES-256-GCM encryption key (min 32 chars) |
| IGNITER_BASE_URL | Recommended | Base URL for OAuth/webhook URL generation |
| IGNITER_BASE_PATH | Optional | Base path prefix (e.g. /api/v1) |
Encryption
Use built-in AES-256-GCM or your own encrypt/decrypt callbacks.
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.withEncrypt(["apiKey", "refreshToken"], {
encrypt: async (value) => encryptValue(value),
decrypt: async (value) => decryptValue(value),
})
.addScope("organization", { required: true })
.addConnector("billing", billingConnector)
.build();🧠 Core Concepts Deep Dive
Connectors
Connectors define schemas, metadata, actions, OAuth, and webhooks.
const slack = IgniterConnector.create()
.withConfig(
z.object({
webhookUrl: z.string().url(),
channel: z.string(),
}),
)
.withMetadata(
z.object({ name: z.string(), icon: z.string() }),
{ name: "Slack", icon: "slack.svg" },
)
.addAction("postMessage", {
input: z.object({ text: z.string() }),
handler: async ({ input, config }) => {
await fetch(config.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ channel: config.channel, text: input.text }),
});
},
})
.build();Scopes
Scopes model multi-tenancy. A required scope needs an identity; optional scopes can omit it.
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addScope("user", { required: true })
.addScope("system", { required: false })
.addConnector("slack", slack)
.build();
const orgScoped = connectors.scope("organization", "org_123");
const systemScoped = connectors.scope("system");OAuth Connectors (Default Config Required)
OAuth connectors store tokens automatically after callback. If you need extra config, use withDefaultConfig().
const mailchimp = IgniterConnector.create()
.withConfig(z.object({ dc: z.string() }))
.withDefaultConfig({ dc: "us6" })
.withOAuth({
authorizationUrl: "https://login.mailchimp.com/oauth2/authorize",
tokenUrl: "https://login.mailchimp.com/oauth2/token",
clientId: process.env.MAILCHIMP_CLIENT_ID!,
clientSecret: process.env.MAILCHIMP_CLIENT_SECRET!,
scopes: [],
})
.addAction("lists", {
input: z.object({}),
handler: async ({ config, oauth }) => {
const response = await fetch(
`https://${config.dc}.api.mailchimp.com/3.0/lists`,
{
headers: { Authorization: `Bearer ${oauth?.accessToken}` },
},
);
return response.json();
},
})
.build();🧪 Examples Library
Below is a curated library of examples you can copy-paste. Each example is verified against the current implementation.
Example 1 — Minimal Connector
const basic = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.addAction("ping", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();Example 2 — Metadata for UI Cards
const metadataConnector = IgniterConnector.create()
.withConfig(z.object({ token: z.string() }))
.withMetadata(
z.object({ name: z.string(), icon: z.string(), docs: z.string() }),
{ name: "Discord", icon: "discord.svg", docs: "https://discord.com" },
)
.addAction("health", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();Example 3 — Context Hook
const withContext = IgniterConnector.create()
.withConfig(z.object({ apiUrl: z.string().url() }))
.onContext(async ({ config }) => ({
client: createHttpClient(config.apiUrl),
}))
.addAction("getStatus", {
input: z.object({}),
handler: async ({ context }) => {
return context.client.get("/status");
},
})
.build();Example 4 — Validate Config Before Connect
const validated = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.onValidate(async ({ config }) => {
const ok = await testKey(config.apiKey);
if (!ok) throw new Error("Invalid API key");
})
.addAction("profile", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();Example 5 — OAuth Connector (PKCE)
const oauthConnector = IgniterConnector.create()
.withConfig(z.object({ workspaceId: z.string() }))
.withDefaultConfig({ workspaceId: "default" })
.withOAuth({
authorizationUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
pkce: true,
scopes: ["read", "write"],
})
.addAction("me", {
input: z.object({}),
handler: async ({ oauth }) => ({ token: oauth?.accessToken }),
})
.build();Example 6 — Custom Token Parsing
const customToken = IgniterConnector.create()
.withConfig(z.object({}))
.withDefaultConfig({})
.withOAuth({
authorizationUrl: "https://provider.com/auth",
tokenUrl: "https://provider.com/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
parseTokenResponse: (response) => ({
accessToken: String(response.token),
refreshToken: String(response.refresh),
expiresIn: Number(response.expires),
}),
})
.build();Example 7 — Custom User Info Parsing
const customUserInfo = IgniterConnector.create()
.withConfig(z.object({}))
.withDefaultConfig({})
.withOAuth({
authorizationUrl: "https://provider.com/auth",
tokenUrl: "https://provider.com/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
userInfoUrl: "https://provider.com/me",
parseUserInfo: (response) => ({
id: String(response.user_id),
name: String(response.display_name ?? ""),
email: String(response.email ?? ""),
}),
})
.build();Example 8 — OAuth Refresh Override
const customRefresh = IgniterConnector.create()
.withConfig(z.object({}))
.withDefaultConfig({})
.withOAuth({
authorizationUrl: "https://provider.com/auth",
tokenUrl: "https://provider.com/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
onRefresh: async ({ refreshToken }) => ({
accessToken: await refreshViaCustomApi(refreshToken),
}),
})
.build();Example 9 — Webhook with Signature Verification
const webhookConnector = IgniterConnector.create()
.withConfig(z.object({ webhookSecret: z.string() }))
.withWebhook({
description: "Stripe webhook",
schema: z.object({ type: z.string(), data: z.object({ object: z.any() }) }),
verify: async (request, config) => {
return verifyStripeSignature(request, config.webhookSecret);
},
handler: async ({ payload }) => {
return { received: payload.type };
},
})
.build();Example 10 — Webhook Handler Only
const webhookOnly = IgniterConnector.create()
.withConfig(z.object({}))
.withWebhook({
schema: z.object({ event: z.string() }),
handler: async ({ payload }) => {
console.log("Event:", payload.event);
},
})
.build();Example 11 — Manager List with Counts
const list = await connectors.list({
count: { connections: true },
limit: 10,
offset: 0,
});Example 12 — Manager Get with Counts
const info = await connectors.get("telegram", { count: { connections: true } });Example 13 — Scoped List Filtering
const enabledOnly = await scoped.list({ where: { enabled: true } });
const named = await scoped.list({ where: { name: "Slack" } });Example 14 — Scoped Count
const count = await scoped.count({ where: { enabled: true } });Example 15 — Connect a Connector
await scoped.connect("slack", {
webhookUrl: "https://hooks.slack.com/...",
channel: "#alerts",
});Example 16 — Toggle Connector
await scoped.toggle("slack");
await scoped.toggle("slack", true);Example 17 — Scoped Action Call
const { data, error } = await scoped.action("slack", "postMessage").call({
text: "Deployment complete",
});Example 18 — Manager Action with Default Config
const defaultConnector = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.withDefaultConfig({ apiKey: process.env.API_KEY! })
.addAction("status", {
input: z.object({}),
handler: async ({ config }) => fetchStatus(config.apiKey),
})
.build();
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("system", { required: false })
.addConnector("status", defaultConnector)
.build();
const result = await manager.action("status", "status").call({});Example 19 — onConnect Hook
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.onConnect(async ({ connector, scope, identity }) => {
await audit.log("connector.connected", { connector, scope, identity });
})
.build();Example 20 — onDisconnect Hook
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.onDisconnect(async ({ connector, scope, identity }) => {
await audit.log("connector.disconnected", { connector, scope, identity });
})
.build();Example 21 — onError Hook
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.onError(async ({ error, connector, operation }) => {
await errorTracker.capture(error, { connector, operation });
})
.build();Example 22 — Global Event Handler
const subscription = connectors.on(async (event) => {
console.log("Event:", event.type, event.connector);
});
subscription.unsubscribe();Example 23 — Scoped Event Handler
const scopedSubscription = scoped.on((event) => {
if (event.type === "action.completed") {
console.log("Action finished:", event.action);
}
});
scopedSubscription.unsubscribe();Example 24 — Custom Encryption Logic
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.withEncrypt(["apiKey"], {
encrypt: (value) => customEncrypt(value),
decrypt: (value) => customDecrypt(value),
})
.addScope("organization", { required: true })
.addConnector("billing", billing)
.build();Example 25 — Encrypt/Decrypt Utility
const encrypted = await IgniterConnectorCrypto.encrypt("secret");
const decrypted = await IgniterConnectorCrypto.decrypt(encrypted);Example 26 — Schema Validation Utility
const schema = z.object({ name: z.string() });
const result = await IgniterConnectorSchema.validate(schema, { name: "Igniter" });
if (result.success) {
console.log(result.data);
}Example 27 — Build UI Fields
const fields = IgniterConnectorFields.fromSchema(
z.object({ apiKey: z.string().describe("API key") }),
);Example 28 — Parse OAuth Tokens
const tokens = IgniterConnectorOAuthUtils.parseTokenResponse({
access_token: "abc",
refresh_token: "xyz",
expires_in: 3600,
});Example 29 — Build URLs
const webhookUrl = IgniterConnectorUrl.buildWebhookUrl("stripe", "secret123");
const callbackUrl = IgniterConnectorUrl.buildOAuthCallbackUrl("mailchimp");Example 30 — Prisma Adapter
const adapter = IgniterConnectorPrismaAdapter.create(prisma, { model: "Connector" });Example 31 — Mock Adapter for Tests
const adapter = IgniterConnectorMockAdapter.create();
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.build();Example 32 — Custom Adapter Skeleton
class CustomAdapter extends IgniterConnectorBaseAdapter {
async get(scope, identity, provider) {
return null;
}
async list(scope, identity) {
return [];
}
async save(scope, identity, provider, value, enabled) {
return {
id: "1",
scope,
identity,
provider,
value,
enabled,
createdAt: new Date(),
updatedAt: new Date(),
};
}
async update(scope, identity, provider, data) {
return {
id: "1",
scope,
identity,
provider,
value: data.value ?? {},
enabled: data.enabled ?? true,
createdAt: new Date(),
updatedAt: new Date(),
};
}
async delete() {}
async countConnections() {
return 0;
}
}Example 33 — Telemetry Setup
import { IgniterTelemetry } from "@igniter-js/telemetry";
import { IgniterConnectorsTelemetryEvents } from "@igniter-js/connectors/telemetry";
const telemetry = IgniterTelemetry.create()
.withService("api")
.addEvents(IgniterConnectorsTelemetryEvents)
.withRedaction({
denylistKeys: ["config", "accessToken", "refreshToken", "payload"],
hashKeys: ["ctx.connector.identity"],
})
.build();Example 34 — OAuth Callback Handling (Request)
export async function GET(request: Request): Promise<Response> {
return connectors.handle("oauth.callback", request);
}Example 35 — Webhook Handling (Request)
export async function POST(request: Request): Promise<Response> {
return connectors.handle("webhook", request);
}Example 36 — Get Webhook Secret After Connect
await scoped.connect("stripe", { webhookSecret: "whsec_123" });
const instance = await scoped.get("stripe");
const secret = instance?.config?.webhook?.secret as string | undefined;
const webhookUrl = secret
? IgniterConnectorUrl.buildWebhookUrl("stripe", secret)
: null;Example 37 — Manager Emit (Custom Event)
await connectors.emit({
type: "connector.updated",
connector: "slack",
scope: "organization",
identity: "org_123",
timestamp: new Date(),
});Example 38 — Validate OAuth Tokens Expiration
const expired = IgniterConnectorOAuthUtils.isExpired({
accessToken: "token",
expiresAt: new Date(Date.now() - 1000),
});Example 39 — Manual URL Base Configuration
IgniterConnectorUrl.setBaseUrl("https://app.example.com");
const callback = IgniterConnectorUrl.buildOAuthCallbackUrl("slack");Example 40 — Mask Sensitive Fields
const masked = IgniterConnectorCrypto.maskFields(
{ apiKey: "sk_test_123456" },
["apiKey"],
);🧩 Real-World Examples
Below are production-style scenarios you can adapt. Each example uses only APIs in this package.
1) E-commerce: Order Status Notifications
const orderConnector = IgniterConnector.create()
.withConfig(z.object({ webhookUrl: z.string().url() }))
.addAction("notify", {
input: z.object({ orderId: z.string(), status: z.string() }),
handler: async ({ input, config }) => {
await fetch(config.webhookUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(input),
});
return { ok: true };
},
})
.build();
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("orders", orderConnector)
.build();
await connectors.scope("organization", "shop_001")
.action("orders", "notify")
.call({ orderId: "A-123", status: "shipped" });2) Fintech: Bank Sync via OAuth
const bank = IgniterConnector.create()
.withConfig(z.object({ region: z.string() }))
.withDefaultConfig({ region: "us" })
.withOAuth({
authorizationUrl: "https://bank.com/oauth/authorize",
tokenUrl: "https://bank.com/oauth/token",
clientId: process.env.BANK_CLIENT_ID!,
clientSecret: process.env.BANK_CLIENT_SECRET!,
})
.addAction("accounts", {
input: z.object({}),
handler: async ({ oauth }) => {
const response = await fetch("https://bank.com/api/accounts", {
headers: { Authorization: `Bearer ${oauth?.accessToken}` },
});
return response.json();
},
})
.build();
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("bank", bank)
.build();
const scoped = manager.scope("organization", "fin_001");
const response = await scoped.connect("bank", { redirectUri: "https://app.com/callback" });3) SaaS: Usage Metrics Ingestion Webhook
const metrics = IgniterConnector.create()
.withConfig(z.object({ webhookSecret: z.string() }))
.withWebhook({
schema: z.object({ userId: z.string(), usage: z.number() }),
handler: async ({ payload }) => {
await storeUsage(payload.userId, payload.usage);
},
})
.build();
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("metrics", metrics)
.build();4) Support: Ticket Sync With Context Hook
const tickets = IgniterConnector.create()
.withConfig(z.object({ apiUrl: z.string().url(), apiKey: z.string() }))
.onContext(async ({ config }) => ({
client: createHttpClient(config.apiUrl, config.apiKey),
}))
.addAction("create", {
input: z.object({ subject: z.string(), body: z.string() }),
handler: async ({ context, input }) => {
return context.client.post("/tickets", input);
},
})
.build();5) Marketing: Campaign Publisher
const campaigns = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.addAction("publish", {
input: z.object({ id: z.string() }),
handler: async ({ input, config }) => {
return publishCampaign(config.apiKey, input.id);
},
})
.build();6) DevOps: Incident Alerts
const pager = IgniterConnector.create()
.withConfig(z.object({ integrationKey: z.string() }))
.addAction("alert", {
input: z.object({ message: z.string() }),
handler: async ({ input, config }) => {
await fetch("https://events.pagerduty.com/v2/enqueue", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ routing_key: config.integrationKey, payload: { summary: input.message } }),
});
},
})
.build();📡 Telemetry
When you call .withTelemetry() on the manager, all connector events are automatically emitted.
Setup
import { IgniterTelemetry } from "@igniter-js/telemetry";
import { IgniterConnectorsTelemetryEvents } from "@igniter-js/connectors/telemetry";
const telemetry = IgniterTelemetry.create()
.withService("my-api")
.addEvents(IgniterConnectorsTelemetryEvents)
.withRedaction({
denylistKeys: [
"config",
"accessToken",
"refreshToken",
"clientSecret",
"apiKey",
"token",
"secret",
"password",
"payload",
"input",
"output",
"userInfo",
],
hashKeys: ["ctx.connector.identity"],
})
.build();Event Groups
igniter.connectors.connector.*igniter.connectors.oauth.*igniter.connectors.action.*igniter.connectors.webhook.*igniter.connectors.adapter.*igniter.connectors.error.*
🧱 Adapters
Prisma Adapter
import { IgniterConnectorPrismaAdapter } from "@igniter-js/connectors/adapters";
const adapter = IgniterConnectorPrismaAdapter.create(prisma, { model: "Connector" });Mock Adapter (Testing)
import { IgniterConnectorMockAdapter } from "@igniter-js/connectors/adapters";
const adapter = IgniterConnectorMockAdapter.create();Custom Adapter Contract
import { IgniterConnectorBaseAdapter } from "@igniter-js/connectors/adapters";
class DynamoAdapter extends IgniterConnectorBaseAdapter {
async get(scope, identity, provider) {
return null;
}
async list(scope, identity) {
return [];
}
async save(scope, identity, provider, value, enabled) {
return { id: "1", scope, identity, provider, value, enabled, createdAt: new Date(), updatedAt: new Date() };
}
async update(scope, identity, provider, data) {
return { id: "1", scope, identity, provider, value: data.value ?? {}, enabled: data.enabled ?? true, createdAt: new Date(), updatedAt: new Date() };
}
async delete() {}
async countConnections() { return 0; }
}📚 API Reference
Connector Builder (IgniterConnector)
| Method | Description |
| --- | --- |
| IgniterConnector.create() | Create a new connector builder |
| .withConfig(schema) | Set configuration schema |
| .withMetadata(schema, value) | Set metadata schema + value |
| .withDefaultConfig(config) | Provide default config (required for OAuth-only config) |
| .withOAuth(options) | Configure OAuth flow |
| .withWebhook(options) | Configure webhook validation + handler |
| .onContext(handler) | Provide action context |
| .onValidate(handler) | Validate config on connect |
| .addAction(key, options) | Add a typed action |
| .build() | Build connector definition |
Manager Builder (IgniterConnectorManager)
| Method | Description |
| --- | --- |
| IgniterConnectorManager.create() | Create builder |
| .withDatabase(adapter) | Required adapter |
| .withLogger(logger) | Optional logger |
| .withTelemetry(telemetry) | Optional telemetry |
| .withEncrypt(fields, callbacks?) | Encryption config |
| .addScope(key, options) | Add a scope |
| .addConnector(key, connector) | Register a connector |
| .onConnect(handler) | Lifecycle hook |
| .onDisconnect(handler) | Lifecycle hook |
| .onError(handler) | Lifecycle hook |
| .on(handler) | Global event handler |
| .build() | Build manager |
| .getScopes() | Get scope types (for inference) |
| .getConnectors() | Get connector types (for inference) |
Manager Instance (returned by .build())
| Method | Description |
| --- | --- |
| .scope(scope, identity?) | Create a scoped instance |
| .list(options?) | List connector metadata |
| .get(key, options?) | Get connector metadata |
| .action(connector, action) | Run action using defaultConfig |
| .handle("oauth.callback"|"webhook", request) | Handle OAuth callback or webhook |
| .on(handler) | Subscribe to events |
| .emit(event) | Emit event (also telemetry) |
| .encrypt(value) / .decrypt(value) | Encrypt/decrypt value |
| .encryptConfig(config) / .decryptConfig(config) | Encrypt/decrypt config |
Scoped Instance (IgniterConnectorScoped)
| Method | Description |
| --- | --- |
| .list(options?) | List connectors for scope |
| .get(key) | Get connector instance |
| .count(options?) | Count connected connectors |
| .connect(key, config) | Connect a connector |
| .disconnect(key) | Disconnect |
| .toggle(key, enabled?) | Enable/disable |
| .action(key, action) | Execute action |
| .on(handler) | Subscribe to scoped events |
🧬 Type Inference
import { IgniterConnectorManager, $Infer } from "@igniter-js/connectors";
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("telegram", telegram)
.build();
type Types = $Infer<typeof connectors>;
type ScopeKey = Types["ScopeKey"]; // "organization"
type ConnectorKey = Types["ConnectorKey"]; // "telegram"
type TelegramConfig = Types["Config"]["telegram"]; // { botToken: string; chatId: string }🧯 Error Handling
import {
IgniterConnectorError,
IGNITER_CONNECTOR_ERROR_CODES,
} from "@igniter-js/connectors";
try {
await scoped.action("telegram", "sendMessage").call({ message: "" });
} catch (error) {
if (error instanceof IgniterConnectorError) {
if (error.code === IGNITER_CONNECTOR_ERROR_CODES.CONNECTOR_ACTION_INPUT_INVALID) {
console.error("Invalid input:", error.metadata);
}
}
}🧪 Testing
Unit Test with Mock Adapter
import { describe, it, expect } from "vitest";
import { IgniterConnectorManager } from "@igniter-js/connectors";
import { IgniterConnectorMockAdapter } from "@igniter-js/connectors/adapters";
describe("connectors", () => {
it("connects and performs actions", async () => {
const adapter = IgniterConnectorMockAdapter.create();
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.build();
const scoped = manager.scope("organization", "org_1");
await scoped.connect("slack", { webhookUrl: "https://...", channel: "#alerts" });
const { data, error } = await scoped.action("slack", "postMessage").call({
text: "Test",
});
expect(error).toBeUndefined();
expect(data).toBeDefined();
});
});✅ Best Practices
| ✅ Do | Why |
| --- | --- |
| Encrypt sensitive fields | Prevent secrets from leaking |
| Use scopes for tenant isolation | Avoid cross-tenant access |
| Add telemetry with redaction | Observability without leaks |
| Use mock adapter in tests | Faster and deterministic |
| Use withDefaultConfig() for OAuth-only inputs | OAuth connect does not accept config |
| ❌ Don’t | Why |
| --- | --- |
| Store raw secrets in config | Config is persisted |
| Skip IGNITER_SECRET in production | Encryption will fail |
| Pass OAuth config to connect() | OAuth connect only accepts redirectUri |
| Assume webhook URL is auto-returned | Build it from stored secret |
🧩 Framework Integration
Next.js Route Handlers
export async function GET(request: Request) {
return connectors.handle("oauth.callback", request);
}
export async function POST(request: Request) {
return connectors.handle("webhook", request);
}Express
app.get("/api/v1/connectors/:key/oauth/callback", async (req, res) => {
const request = new Request(req.protocol + "://" + req.get("host") + req.originalUrl, {
method: "GET",
headers: req.headers as Record<string, string>,
});
const response = await connectors.handle("oauth.callback", request);
res.status(response.status);
response.headers.forEach((value, key) => res.setHeader(key, value));
res.send(await response.text());
});
app.post("/api/v1/connectors/:key/webhook/:secret", async (req, res) => {
const request = new Request(req.protocol + "://" + req.get("host") + req.originalUrl, {
method: "POST",
headers: req.headers as Record<string, string>,
body: JSON.stringify(req.body),
});
const response = await connectors.handle("webhook", request);
res.status(response.status).send(await response.text());
});🧩 Troubleshooting
IGNITER_SECRET missing
Symptom: Encryption fails with CONNECTOR_ENCRYPTION_SECRET_REQUIRED.
Fix: Set a 32+ char IGNITER_SECRET or supply custom encrypt/decrypt callbacks.
export IGNITER_SECRET="your-32-character-secret-key-here"OAuth callback errors
Symptom: OAuth flow fails with CONNECTOR_OAUTH_STATE_INVALID.
Fix: Ensure cookies are preserved between connect and callback. The callback handler reads igniter_oauth_<connector> cookie.
📦 Extended Examples Library (Advanced)
Use these to cover advanced patterns and edge cases.
Example 41 — OAuth Connect in a Controller
const scoped = connectors.scope("organization", "org_123");
const response = await scoped.connect("bank", { redirectUri: "https://app.com/callback" });Example 42 — Fetch Metadata with Counts
const catalog = await connectors.list({ count: { connections: true } });
const mailchimp = await connectors.get("mailchimp", { count: { connections: true } });Example 43 — Filter Connected Connectors by Name
const list = await scoped.list({ where: { name: "Slack" } });Example 44 — Build Webhook URL After Connection
const instance = await scoped.get("stripe");
const secret = instance?.config?.webhook?.secret as string | undefined;
const webhookUrl = secret
? IgniterConnectorUrl.buildWebhookUrl("stripe", secret)
: null;Example 45 — Custom Base Path for URLs
process.env.IGNITER_BASE_URL = "https://app.example.com";
process.env.IGNITER_BASE_PATH = "/api/v1";
const callbackUrl = IgniterConnectorUrl.buildOAuthCallbackUrl("stripe");Example 46 — Use validateOrThrow
const schema = z.object({ id: z.string() });
const valid = await IgniterConnectorSchema.validateOrThrow(schema, { id: "abc" });Example 47 — Generate Form Fields from Schema
const schema = z.object({
apiKey: z.string().describe("API key"),
region: z.enum(["us", "eu"]).describe("Region"),
});
const fields = IgniterConnectorFields.fromSchema(schema);Example 48 — Merge Fields With Stored Config
const fields = IgniterConnectorFields.fromSchema(schema);
const withValues = IgniterConnectorFields.mergeWithConfig(fields, {
apiKey: "sk_123",
region: "us",
});Example 49 — Mask Fields for Logging
const masked = IgniterConnectorCrypto.maskFields(
{ apiKey: "sk_test_123456" },
["apiKey"],
"*",
3,
);Example 50 — Parse OAuth User Info
const user = IgniterConnectorOAuthUtils.parseUserInfo({
sub: "user_123",
name: "Ava",
email: "[email protected]",
});Example 51 — Verify Token Refresh Capability
const canRefresh = IgniterConnectorOAuthUtils.canRefresh({
accessToken: "token",
refreshToken: "refresh",
});Example 52 — Create Scoped Helper Function
type Scoped = $InferScoped<typeof connectors>;
async function sendAlert(scoped: Scoped, message: string) {
await scoped.action("slack", "postMessage").call({ text: message });
}Example 53 — Using $InferConfig
type SlackConfig = $InferConfig<typeof connectors, "slack">;
const config: SlackConfig = { webhookUrl: "https://...", channel: "#ops" };Example 54 — Using $InferActionKeys
type SlackActions = $InferActionKeys<typeof connectors, "slack">;
const action: SlackActions = "postMessage";Example 55 — Event Filtering
connectors.on((event) => {
if (event.type === "connector.connected") {
console.log("Connected:", event.connector);
}
});Example 56 — Manual Telemetry Emit
await connectors.emit({
type: "error.occurred",
connector: "slack",
scope: "organization",
identity: "org_123",
timestamp: new Date(),
error: new Error("Boom"),
errorCode: "CUSTOM_ERROR",
errorMessage: "Something failed",
operation: "action",
});Example 57 — Count Connections Per Connector
const stats = await connectors.list({ count: { connections: true } });Example 58 — Refresh OAuth Tokens Automatically
const { data } = await scoped.action("bank", "accounts").call({});Example 59 — Use Mock Adapter Call Counters
const adapter = IgniterConnectorMockAdapter.create();
await adapter.save("org", "id", "slack", {}, true);
console.log(adapter.calls.save); // 1Example 60 — Use Mock Adapter Clear
adapter.clear();Example 61 — Build Authorization URL Manually
const url = IgniterConnectorOAuthUtils.buildAuthUrl(
"https://provider.com/oauth/authorize",
{
client_id: "client",
redirect_uri: "https://app.com/callback",
response_type: "code",
scope: "read",
state: "random",
},
);Example 62 — Generate OAuth State
const state = IgniterConnectorOAuthUtils.generateState();Example 63 — Generate PKCE Verifier/Challenge
const verifier = IgniterConnectorOAuthUtils.generateCodeVerifier();
const challenge = await IgniterConnectorOAuthUtils.generateCodeChallenge(verifier);Example 64 — Encrypt and Decrypt Fields
const encrypted = await IgniterConnectorCrypto.encryptFields(
{ token: "secret", name: "Test" },
["token"],
);
const decrypted = await IgniterConnectorCrypto.decryptFields(encrypted, ["token"]);Example 65 — Make a System Connector
const systemConnector = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.withDefaultConfig({ apiKey: process.env.SYSTEM_KEY! })
.addAction("status", {
input: z.object({}),
handler: async ({ config }) => getStatus(config.apiKey),
})
.build();
const systemManager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("system", { required: false })
.addConnector("system", systemConnector)
.build();Example 66 — Shared Manager for Multiple Connectors
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.addConnector("telegram", telegram)
.addConnector("stripe", stripe)
.build();Example 67 — Update Enable State Directly (Adapter)
await adapter.update("organization", "org_123", "slack", { enabled: false });Example 68 — Webhook Verification Failure Handling
try {
await connectors.handle("webhook", request);
} catch (error) {
console.error("Webhook failed:", error);
}Example 69 — Build Connector List UI
const list = await connectors.list();
const uiItems = list.map((item) => ({
key: item.key,
type: item.type,
name: item.metadata.name,
}));Example 70 — Connection Health Check
const instance = await scoped.get("slack");
const enabled = instance?.enabled ?? false;🧾 Full API Reference (Detailed)
IgniterConnectorManagerCore (Manager Instance)
scope(scopeKey, identity?)
- Validates scope key
- Requires identity if scope is
required: true - Returns
IgniterConnectorScoped
const scoped = connectors.scope("organization", "org_123");list(options?)
- Returns metadata for all registered connectors
- Optional
count.connectionsincludes connection counts
const list = await connectors.list({ limit: 10, offset: 0, count: { connections: true } });get(connectorKey, options?)
- Returns connector metadata or
null
const info = await connectors.get("slack", { count: { connections: true } });action(connectorKey, actionKey)
- Uses
defaultConfigfrom connector definition - Returns
{ data, error }
const { data, error } = await connectors.action("system", "status").call({});handle("oauth.callback" | "webhook", request)
- Parses URL and dispatches OAuth callback or webhook handling
- Expects URLs in
/api/v1/connectors/:key/oauth/callbackor/api/v1/connectors/:key/webhook/:secret
return connectors.handle("oauth.callback", request);emit(event)
- Emits to internal handlers
- Emits telemetry when configured
await connectors.emit({
type: "connector.connected",
connector: "slack",
scope: "organization",
identity: "org_123",
timestamp: new Date(),
});IgniterConnectorScoped
connect(connectorKey, config)
- Validates config schema (custom connectors)
- For OAuth connectors, uses
redirectUriand returnsResponse
await scoped.connect("slack", { webhookUrl: "https://...", channel: "#alerts" });disconnect(connectorKey)
await scoped.disconnect("slack");toggle(connectorKey, enabled?)
await scoped.toggle("slack", false);action(connectorKey, actionKey)
await scoped.action("slack", "postMessage").call({ text: "Hello" });IgniterConnector (Connector Builder)
withConfig(schema)
- Uses
StandardSchemaV1(Zod-compatible)
IgniterConnector.create().withConfig(z.object({ apiKey: z.string() }));withOAuth(options)
- Configures OAuth workflow
IgniterConnector.create().withOAuth({
authorizationUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
});🧯 Error Code Library
Every error code has a clear context, cause, and solution.
CONNECTOR_NOT_FOUND
- Context: Connector key not registered
- Cause: Calling
.connect()or.action()for an unknown key - Mitigation: Validate connector key before use
- Solution: Register the connector with
.addConnector()
CONNECTOR_NOT_CONNECTED
- Context: Action attempted without prior connect
- Cause: No stored record for scope + connector
- Mitigation: Call
.connect()first - Solution: Ensure
scoped.connect()completed
CONNECTOR_ALREADY_CONNECTED
- Context: Attempted to connect twice
- Cause: Record already exists
- Mitigation: Check
scoped.get()before connect - Solution: Update instead of connect
CONNECTOR_CONFIG_INVALID
- Context: Config schema validation failed
- Cause: Missing or invalid config fields
- Mitigation: Validate config client-side
- Solution: Fix config to match schema
CONNECTOR_DEFAULT_CONFIG_REQUIRED
- Context: Manager
.action()used without defaultConfig - Cause: No
withDefaultConfig()in connector definition - Mitigation: Use scoped actions
- Solution: Add
withDefaultConfig()
CONNECTOR_ACTION_NOT_FOUND
- Context: Action key not registered
- Cause: Typo or missing action definition
- Mitigation: Use
$InferActionKeysfor type safety - Solution: Add
.addAction()or correct key
CONNECTOR_ACTION_INPUT_INVALID
- Context: Action input schema failed
- Cause: Wrong input type or missing fields
- Mitigation: Validate input before calling
- Solution: Fix input to match schema
CONNECTOR_ACTION_OUTPUT_INVALID
- Context: Action output schema mismatch
- Cause: Handler returns wrong shape
- Mitigation: Align handler output with schema
- Solution: Fix handler return type
CONNECTOR_ACTION_FAILED
- Context: Action handler threw
- Cause: API failure or unexpected exception
- Mitigation: Add error handling in handler
- Solution: Catch/transform errors
CONNECTOR_SCOPE_INVALID
- Context: Unknown scope key
- Cause: Calling
.scope()with unregistered scope - Mitigation: Use
$InferScopeKey - Solution: Add
.addScope()
CONNECTOR_SCOPE_IDENTIFIER_REQUIRED
- Context: Missing identity for required scope
- Cause:
required: truescope but identity not provided - Mitigation: Provide identity
- Solution: Pass identity or make scope optional
CONNECTOR_DATABASE_REQUIRED
- Context: Database adapter missing
- Cause:
.withDatabase()not called - Mitigation: Ensure adapter is configured
- Solution: Add
.withDatabase()
CONNECTOR_DATABASE_FAILED
- Context: Adapter error
- Cause: DB connectivity or adapter bug
- Mitigation: Wrap adapter calls with retries
- Solution: Fix adapter implementation
CONNECTOR_OAUTH_NOT_CONFIGURED
- Context: OAuth operation on non-OAuth connector
- Cause: Missing
.withOAuth() - Mitigation: Verify connector definition
- Solution: Add
.withOAuth()
CONNECTOR_OAUTH_STATE_INVALID
- Context: OAuth state mismatch
- Cause: Cookies dropped or incorrect state
- Mitigation: Preserve cookies
- Solution: Retry OAuth flow
CONNECTOR_OAUTH_TOKEN_FAILED
- Context: Token exchange failure
- Cause: Invalid credentials or provider error
- Mitigation: Check client ID/secret
- Solution: Fix OAuth credentials
CONNECTOR_OAUTH_PARSE_TOKEN_FAILED
- Context: Token response format unsupported
- Cause: Provider response is non-standard
- Mitigation: Provide
parseTokenResponse - Solution: Implement custom parser
CONNECTOR_OAUTH_PARSE_USERINFO_FAILED
- Context: User info response format unsupported
- Cause: Provider response is non-standard
- Mitigation: Provide
parseUserInfo - Solution: Implement custom parser
CONNECTOR_OAUTH_REFRESH_FAILED
- Context: Refresh token invalid
- Cause: Token revoked or expired
- Mitigation: Reconnect via OAuth
- Solution: Start new OAuth flow
CONNECTOR_WEBHOOK_NOT_CONFIGURED
- Context: Webhook handler called without webhook config
- Cause: Missing
.withWebhook() - Mitigation: Add webhook config
- Solution: Use
.withWebhook()
CONNECTOR_WEBHOOK_VALIDATION_FAILED
- Context: Webhook payload invalid
- Cause: Payload does not match schema
- Mitigation: Validate payload before sending
- Solution: Fix sender or schema
CONNECTOR_WEBHOOK_VERIFICATION_FAILED
- Context: Signature verification failed
- Cause: Wrong secret or signature
- Mitigation: Ensure correct secret
- Solution: Fix verification logic
CONNECTOR_ENCRYPT_FAILED
- Context: Encryption failure
- Cause: Invalid secret or custom encrypt error
- Mitigation: Validate encryption keys
- Solution: Fix
IGNITER_SECRETor custom encrypt
CONNECTOR_DECRYPT_FAILED
- Context: Decryption failure
- Cause: Invalid secret or corrupted data
- Mitigation: Validate encryption keys
- Solution: Fix
IGNITER_SECRETor custom decrypt
CONNECTOR_ENCRYPTION_SECRET_REQUIRED
- Context: Missing
IGNITER_SECRET - Cause: Encryption attempted without secret
- Mitigation: Set env variable
- Solution: Provide secret or custom encrypt/decrypt
CONNECTOR_BUILD_CONFIG_REQUIRED
- Context:
withConfig()missing - Cause: Connector definition incomplete
- Mitigation: Always call
.withConfig() - Solution: Add config schema
CONNECTOR_BUILD_SCOPES_REQUIRED
- Context: No scopes configured
- Cause: Builder missing
.addScope() - Mitigation: Define at least one scope
- Solution: Add a scope
CONNECTOR_BUILD_CONNECTORS_REQUIRED
- Context: No connectors configured
- Cause: Builder missing
.addConnector() - Mitigation: Register at least one connector
- Solution: Add a connector
📡 Telemetry Event Catalog
Each event emitted by the manager also maps to telemetry. Use this to build dashboards.
Connector Lifecycle
igniter.connectors.connector.connectedigniter.connectors.connector.disconnectedigniter.connectors.connector.enabledigniter.connectors.connector.disabledigniter.connectors.connector.updated
OAuth Flow
igniter.connectors.oauth.startedigniter.connectors.oauth.completedigniter.connectors.oauth.refreshedigniter.connectors.oauth.failed
Action Execution
igniter.connectors.action.startedigniter.connectors.action.completedigniter.connectors.action.failed
Webhooks
igniter.connectors.webhook.receivedigniter.connectors.webhook.processedigniter.connectors.webhook.failed
Adapter
igniter.connectors.adapter.getigniter.connectors.adapter.listigniter.connectors.adapter.upsertigniter.connectors.adapter.updateigniter.connectors.adapter.delete
Errors
igniter.connectors.error.occurred
🧩 Security Notes
- Always use encryption for secrets.
- Never emit raw config or payloads in telemetry.
- Hash scope identities if they contain PII.
- Consider separate scopes for internal vs user integrations.
📚 Additional Recipes
Recipe: UI Integration Config Form
const fields = IgniterConnectorFields.fromSchema(connector.configSchema);
const formSchema = fields.map((field) => ({
label: field.label,
name: field.key,
required: field.required,
}));Recipe: Batch Message Sender
async function sendBatch(scoped: $InferScoped<typeof connectors>, messages: string[]) {
for (const message of messages) {
await scoped.action("slack", "postMessage").call({ text: message });
}
}Recipe: Custom Adapter With Caching
class CachedAdapter extends IgniterConnectorBaseAdapter {
private cache = new Map<string, IgniterConnectorRecord>();
async get(scope, identity, provider) {
const key = `${scope}:${identity}:${provider}`;
return this.cache.get(key) ?? null;
}
async list() { return []; }
async save(scope, identity, provider, value, enabled) {
const record = {
id: "cached",
scope,
identity,
provider,
value,
enabled,
createdAt: new Date(),
updatedAt: new Date(),
};
this.cache.set(`${scope}:${identity}:${provider}`, record);
return record;
}
async update(scope, identity, provider, data) {
return this.save(scope, identity, provider, data.value ?? {}, data.enabled ?? true);
}
async delete() {}
async countConnections() { return 0; }
}📚 Examples Library (Continued)
Example 71 — Scoped Connect in a Job
await connectors.scope("organization", "org_999")
.connect("telegram", { botToken: "token", chatId: "123" });Example 72 — Scoped Disconnect in a Cleanup
await connectors.scope("organization", "org_999")
.disconnect("telegram");Example 73 — Create a Connector With Multiple Actions
const crm = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.addAction("createContact", {
input: z.object({ email: z.string().email() }),
handler: async ({ input }) => ({ id: `c_${input.email}` }),
})
.addAction("listContacts", {
input: z.object({}),
handler: async () => ({ items: [] as Array<{ id: string }> }),
})
.build();Example 74 — Scoped Action Error Handling
const { data, error } = await scoped.action("crm", "createContact").call({
email: "invalid",
});
if (error) {
console.error("Action failed:", error.message);
}Example 75 — onValidate With Remote Ping
const pinged = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.onValidate(async ({ config }) => {
const ok = await pingProvider(config.apiKey);
if (!ok) throw new Error("Invalid api key");
})
.addAction("status", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();Example 76 — Event Stream to Logger
connectors.on((event) => {
console.log(`[${event.type}] ${event.connector}`);
});Example 77 — Scoped Event Stream to Logger
scoped.on((event) => {
console.log(`[${event.type}] ${event.connector}`);
});Example 78 — Use IgniterConnectorUrl.parseWebhookUrl
const parsed = IgniterConnectorUrl.parseWebhookUrl(
"https://app.example.com/api/v1/connectors/stripe/webhook/secret123",
);Example 79 — Use IgniterConnectorUrl.parseOAuthCallbackUrl
const parsed = IgniterConnectorUrl.parseOAuthCallbackUrl(
"https://app.example.com/api/v1/connectors/stripe/oauth/callback?code=123",
);Example 80 — Check IgniterConnectorSchema.isSchema
const isSchema = IgniterConnectorSchema.isSchema(z.string());Example 81 — Use getScopes() for Type Inference
const scopes = connectors.getScopes();Example 82 — Use getConnectors() for Type Inference
const defs = connectors.getConnectors();Example 83 — Build a Reporting Dashboard
const list = await connectors.list({ count: { connections: true } });
const metrics = list.map((item) => ({
connector: item.key,
connections: item.connections ?? 0,
}));Example 84 — Action with Output Schema
const outputSchema = z.object({ id: z.string() });
const withOutput = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.addAction("create", {
input: z.object({ name: z.string() }),
output: outputSchema,
handler: async ({ input }) => ({ id: `id_${input.name}` }),
})
.build();Example 85 — Default Config for System Connectors
const internal = IgniterConnector.create()
.withConfig(z.object({ key: z.string() }))
.withDefaultConfig({ key: process.env.INTERNAL_KEY! })
.addAction("ping", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();Example 86 — Disable Connector by Policy
if (policy.shouldDisable(connectorKey)) {
await scoped.toggle(connectorKey, false);
}Example 87 — OAuth With Extra Params
const oauthExtra = IgniterConnector.create()
.withConfig(z.object({}))
.withDefaultConfig({})
.withOAuth({
authorizationUrl: "https://provider.com/oauth/authorize",
tokenUrl: "https://provider.com/oauth/token",
clientId: process.env.CLIENT_ID!,
clientSecret: process.env.CLIENT_SECRET!,
extraParams: { prompt: "consent", access_type: "offline" },
})
.build();Example 88 — Use .withMetadata for Marketplace Cards
const market = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.withMetadata(
z.object({ name: z.string(), website: z.string().url() }),
{ name: "Acme", website: "https://acme.com" },
)
.addAction("ping", { input: z.object({}), handler: async () => ({ ok: true }) })
.build();Example 89 — Build Config Validation Errors
try {
await scoped.connect("slack", { webhookUrl: "not-a-url", channel: "#general" });
} catch (error) {
if (error instanceof IgniterConnectorError) {
console.error(error.code, error.metadata);
}
}Example 90 — Use IgniterConnectorOAuthUtils.parseTokenResponse
const tokens = IgniterConnectorOAuthUtils.parseTokenResponse({
token: "token",
expires: 3600,
});Example 91 — Use Schema Validation in a CLI
const result = await IgniterConnectorSchema.validate(schema, input);
if (!result.success) {
console.error(result.errors);
}Example 92 — Use .action() for Fire-and-Forget
await scoped.action("slack", "postMessage").call({ text: "Deploy done" });Example 93 — Enforce Max Field Size
const schema = z.object({
apiKey: z.string().min(1).max(64),
});Example 94 — Optional Scope for System Tasks
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("system", { required: false })
.addConnector("status", systemConnector)
.build();
const system = manager.scope("system");Example 95 — Check Enabled Flag Before Action
const instance = await scoped.get("slack");
if (!instance?.enabled) {
throw new Error("Connector disabled");
}Example 96 — Custom Logger
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.withLogger({
debug: (...args) => console.debug("[connectors]", ...args),
info: (...args) => console.info("[connectors]", ...args),
warn: (...args) => console.warn("[connectors]", ...args),
error: (...args) => console.error("[connectors]", ...args),
})
.addScope("organization", { required: true })
.addConnector("slack", slack)
.build();Example 97 — Add a Webhook-Only Connector
const webhookOnly = IgniterConnector.create()
.withConfig(z.object({ webhookSecret: z.string() }))
.withWebhook({
schema: z.object({ event: z.string() }),
handler: async ({ payload }) => ({ ok: payload.event }),
})
.build();Example 98 — Enforce Custom Scope Keys
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("workspace", { required: true })
.addConnector("slack", slack)
.build();
const scoped = manager.scope("workspace", "wk_123");Example 99 — Use IgniterConnectorCrypto.isEncrypted
const isEncrypted = IgniterConnectorCrypto.isEncrypted("iv:tag:cipher");Example 100 — Handle Errors with Codes
try {
await scoped.action("slack", "postMessage").call({ text: "" });
} catch (error) {
if (error instanceof IgniterConnectorError) {
console.error("Code:", error.code);
}
}🧾 Telemetry Attribute Reference
Use these attributes when querying your telemetry store.
Base Connector Attributes
ctx.connector.providerctx.connector.scopectx.connector.identity
Connection Attributes
ctx.connector.encryptedctx.connector.encryptedFields
OAuth Attributes
ctx.oauth.authorizationUrlctx.oauth.tokenUrlctx.oauth.pkcectx.oauth.scopesctx.oauth.hasState
Action Attributes
ctx.action.namectx.action.durationMsctx.action.success
Webhook Attributes
ctx.webhook.methodctx.webhook.pathctx.webhook.durationMsctx.webhook.verified
Error Attributes
ctx.error.codectx.error.messagectx.error.operationctx.error.action
Adapter Attributes
ctx.adapter.durationMsctx.adapter.foundctx.adapter.countctx.adapter.inserted
❓ FAQ
1) Do I need Zod?
No. You can use any StandardSchemaV1 compatible library. Zod is the most common.
2) Why does OAuth connect not accept config?
OAuth connect uses redirectUri only. Use withDefaultConfig() for static config values.
3) Can I use this in a browser?
No. The package is server-only and ships a browser shim that throws explicit errors.
4) Where is telemetry emitted?
When you call .withTelemetry() on the manager.
5) Can I disable encryption?
Yes. Skip withEncrypt() and do not access crypto utilities.
6) How are webhooks validated?
Use .withWebhook({ schema, verify }) to validate payload and verify signatures.
7) Do I need a database?
Yes for scoped operations. Manager .action() can run without database using defaultConfig.
8) How do I test without a DB?
Use IgniterConnectorMockAdapter.
9) Can I add custom scopes?
Yes. Scopes are arbitrary keys.
10) Does it refresh OAuth tokens?
Yes. It refreshes tokens when expired and refreshToken is available.
11) Can I override OAuth token parsing?
Yes, use parseTokenResponse.
12) Can I override OAuth user info parsing?
Yes, use parseUserInfo.
13) How do I handle errors globally?
Use .onError() and/or .on() event handlers.
14) Can I emit custom events?
Yes. Use .emit() with IgniterConnectorEvent shape.
15) How do I build webhook URLs?
Use IgniterConnectorUrl.buildWebhookUrl() and the stored webhook.secret.
16) Where is IGNITER_BASE_URL used?
It defines the base URL for OAuth and webhook URLs.
17) Can I run this in Edge runtimes?
The package uses node:crypto in IgniterConnectorCrypto.
18) How do I list all connectors?
Use connectors.list().
19) How do I list connected connectors?
Use scoped.list().
20) What if I need a custom adapter?
Extend IgniterConnectorBaseAdapter and implement required methods.
📘 Glossary
- Connector: A definition that describes config, actions, and optional OAuth/webhook behavior.
- Manager: Runtime that stores connectors, handles events, and produces scoped instances.
- Scope: A tenant boundary such as organization or user.
- Scoped Instance: Accessor for a specific scope + identity.
- Adapter: Storage implementation for connector records.
- Action: Typed operation defined on a connector.
- Webhook: Inbound event flow handled via URL + secret.
- OAuth: Authorization flow for third-party accounts.
- Telemetry: Structured event stream for observability.
📎 Related Packages
📜 License
MIT © Felipe Barcelos# @igniter-js/connectors
Type-safe, multi-tenant connector management library for Igniter.js. Build integrations with third-party services using OAuth, custom configurations, webhooks, and encrypted field storage.
Features
- ✅ Type-Safe Connectors - Full TypeScript inference for configs, actions, and outputs
- ✅ Multi-Tenant Scopes - Organize connectors by organization, user, or custom scopes
- ✅ OAuth Universal - Built-in OAuth 2.0 flow with PKCE support and auto-refresh
- ✅ Field Encryption - AES-256-GCM encryption for sensitive configuration fields
- ✅ Webhook Support - Receive and validate webhooks from integrated services
- ✅ Prisma Adapter - Production-ready database adapter for Prisma ORM
- ✅ Builder Pattern - Fluent API for defining connectors and managers
- ✅ Event System - Subscribe to connector lifecycle events
- ✅ Schema Validation - Runtime validation with StandardSchema (Zod)
- ✅ Telemetry Integration - Built-in observability with automatic event emission
Installation
# npm
npm install @igniter-js/connectors @igniter-js/common
# pnpm
pnpm add @igniter-js/connectors @igniter-js/common
# yarn
yarn add @igniter-js/connectors @igniter-js/common
# bun
bun add @igniter-js/connectors @igniter-js/commonQuick Start
1. Define a Connector
Use the Connector builder to define what a connector needs and can do:
import { Connector } from "@igniter-js/connectors";
import { z } from "zod";
// Define a Telegram connector
const telegramConnector = Connector.create()
.withConfig(
z.object({
botToken: z.string(),
chatId: z.string(),
}),
)
.withMetadata(z.object({ name: z.string(), icon: z.string() }), {
name: "Telegram",
icon: "telegram.svg",
})
.addAction("sendMessage", {
description: "Send a message to a Telegram chat",
input: z.object({
message: z.string(),
parseMode: z.enum(["HTML", "Markdown"]).optional(),
}),
output: z.object({
messageId: z.number(),
}),
handler: async ({ input, config }) => {
const response = await fetch(
`https://