Type-safe, multi-tenant connector management library for Igniter.js with OAuth, encryption, and adapter support
npm install @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
---
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
---
``bashnpm
npm install @igniter-js/connectors @igniter-js/common zod
$3
`typescript
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
$3
`
┌─────────────────────────────────────────────────────────┐
│ Your Application │
├─────────────────────────────────────────────────────────┤
│ scoped.action('telegram','sendMessage').call(...) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ IgniterConnectorManagerCore │
│ - Scopes │
│ - Connectors │
│ - Hooks │
│ - Telemetry │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Adapter Layer │
│ PrismaAdapter | MockAdapter | CustomAdapter │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Storage Backend │
│ Postgres | MySQL | SQLite | Memory | Anything │
└─────────────────────────────────────────────────────────┘
`$3
- 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 IgniterConnectorsTelemetryEvents to emit consistent events.---
🧭 Exports & Subpaths
$3
`typescript
import {
IgniterConnector,
IgniterConnectorManager,
IgniterConnectorManagerBuilder,
IgniterConnectorError,
IGNITER_CONNECTOR_ERROR_CODES,
IgniterConnectorCrypto,
IgniterConnectorFields,
IgniterConnectorSchema,
IgniterConnectorOAuthUtils,
IgniterConnectorUrl,
$Infer,
$InferScoped,
$InferConnectorKey,
$InferScopeKey,
$InferConfig,
$InferActionKeys,
} from "@igniter-js/connectors";
`$3
`typescript
import {
IgniterConnectorBaseAdapter,
IgniterConnectorPrismaAdapter,
IgniterConnectorMockAdapter,
} from "@igniter-js/connectors/adapters";
`$3
`typescript
import { IgniterConnectorsTelemetryEvents } from "@igniter-js/connectors/telemetry";
`---
⚙️ Configuration
$3
| 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) |$3
Use built-in AES-256-GCM or your own encrypt/decrypt callbacks.
`typescript
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
$3
Connectors define schemas, metadata, actions, OAuth, and webhooks.
`typescript
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();
`$3
Scopes model multi-tenancy. A required scope needs an identity; optional scopes can omit it.
`typescript
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");
`$3
OAuth connectors store tokens automatically after callback. If you need extra config, use
withDefaultConfig().`typescript
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.
$3
`typescript
const basic = IgniterConnector.create()
.withConfig(z.object({ apiKey: z.string() }))
.addAction("ping", {
input: z.object({}),
handler: async () => ({ ok: true }),
})
.build();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
const webhookOnly = IgniterConnector.create()
.withConfig(z.object({}))
.withWebhook({
schema: z.object({ event: z.string() }),
handler: async ({ payload }) => {
console.log("Event:", payload.event);
},
})
.build();
`$3
`typescript
const list = await connectors.list({
count: { connections: true },
limit: 10,
offset: 0,
});
`$3
`typescript
const info = await connectors.get("telegram", { count: { connections: true } });
`$3
`typescript
const enabledOnly = await scoped.list({ where: { enabled: true } });
const named = await scoped.list({ where: { name: "Slack" } });
`$3
`typescript
const count = await scoped.count({ where: { enabled: true } });
`$3
`typescript
await scoped.connect("slack", {
webhookUrl: "https://hooks.slack.com/...",
channel: "#alerts",
});
`$3
`typescript
await scoped.toggle("slack");
await scoped.toggle("slack", true);
`$3
`typescript
const { data, error } = await scoped.action("slack", "postMessage").call({
text: "Deployment complete",
});
`$3
`typescript
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({});
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
const subscription = connectors.on(async (event) => {
console.log("Event:", event.type, event.connector);
});subscription.unsubscribe();
`$3
`typescript
const scopedSubscription = scoped.on((event) => {
if (event.type === "action.completed") {
console.log("Action finished:", event.action);
}
});scopedSubscription.unsubscribe();
`$3
`typescript
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.withEncrypt(["apiKey"], {
encrypt: (value) => customEncrypt(value),
decrypt: (value) => customDecrypt(value),
})
.addScope("organization", { required: true })
.addConnector("billing", billing)
.build();
`$3
`typescript
const encrypted = await IgniterConnectorCrypto.encrypt("secret");
const decrypted = await IgniterConnectorCrypto.decrypt(encrypted);
`$3
`typescript
const schema = z.object({ name: z.string() });
const result = await IgniterConnectorSchema.validate(schema, { name: "Igniter" });
if (result.success) {
console.log(result.data);
}
`$3
`typescript
const fields = IgniterConnectorFields.fromSchema(
z.object({ apiKey: z.string().describe("API key") }),
);
`$3
`typescript
const tokens = IgniterConnectorOAuthUtils.parseTokenResponse({
access_token: "abc",
refresh_token: "xyz",
expires_in: 3600,
});
`$3
`typescript
const webhookUrl = IgniterConnectorUrl.buildWebhookUrl("stripe", "secret123");
const callbackUrl = IgniterConnectorUrl.buildOAuthCallbackUrl("mailchimp");
`$3
`typescript
const adapter = IgniterConnectorPrismaAdapter.create(prisma, { model: "Connector" });
`$3
`typescript
const adapter = IgniterConnectorMockAdapter.create();
const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.build();
`$3
`typescript
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;
}
}
`$3
`typescript
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();
`$3
`typescript
export async function GET(request: Request): Promise {
return connectors.handle("oauth.callback", request);
}
`$3
`typescript
export async function POST(request: Request): Promise {
return connectors.handle("webhook", request);
}
`$3
`typescript
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;
`$3
`typescript
await connectors.emit({
type: "connector.updated",
connector: "slack",
scope: "organization",
identity: "org_123",
timestamp: new Date(),
});
`$3
`typescript
const expired = IgniterConnectorOAuthUtils.isExpired({
accessToken: "token",
expiresAt: new Date(Date.now() - 1000),
});
`$3
`typescript
IgniterConnectorUrl.setBaseUrl("https://app.example.com");
const callback = IgniterConnectorUrl.buildOAuthCallbackUrl("slack");
`$3
`typescript
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.
$3
`typescript
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" });
`$3
`typescript
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
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
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.$3
`typescript
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();
`$3
-
igniter.connectors.connector.*
- igniter.connectors.oauth.*
- igniter.connectors.action.*
- igniter.connectors.webhook.*
- igniter.connectors.adapter.*
- igniter.connectors.error.*---
🧱 Adapters
$3
`typescript
import { IgniterConnectorPrismaAdapter } from "@igniter-js/connectors/adapters";
const adapter = IgniterConnectorPrismaAdapter.create(prisma, { model: "Connector" });
`$3
`typescript
import { IgniterConnectorMockAdapter } from "@igniter-js/connectors/adapters";
const adapter = IgniterConnectorMockAdapter.create();
`$3
`typescript
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
$3
| 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 |$3
| 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) |$3
| 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 |$3
| 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
`typescript
import { IgniterConnectorManager, $Infer } from "@igniter-js/connectors";const connectors = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("telegram", telegram)
.build();
type Types = $Infer;
type ScopeKey = Types["ScopeKey"]; // "organization"
type ConnectorKey = Types["ConnectorKey"]; // "telegram"
type TelegramConfig = Types["Config"]["telegram"]; // { botToken: string; chatId: string }
`---
🧯 Error Handling
`typescript
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
$3
`typescript
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
$3
`typescript
export async function GET(request: Request) {
return connectors.handle("oauth.callback", request);
}export async function POST(request: Request) {
return connectors.handle("webhook", request);
}
`$3
`typescript
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,
});
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,
body: JSON.stringify(req.body),
});
const response = await connectors.handle("webhook", request);
res.status(response.status).send(await response.text());
});
`---
🧩 Troubleshooting
$3
Symptom: Encryption fails with
CONNECTOR_ENCRYPTION_SECRET_REQUIRED.Fix: Set a 32+ char
IGNITER_SECRET or supply custom encrypt/decrypt callbacks.`bash
export IGNITER_SECRET="your-32-character-secret-key-here"
`$3
Symptom: OAuth flow fails with
CONNECTOR_OAUTH_STATE_INVALID.Fix: Ensure cookies are preserved between connect and callback. The callback handler reads
igniter_oauth_ cookie.---
📦 Extended Examples Library (Advanced)
Use these to cover advanced patterns and edge cases.
$3
`typescript
const scoped = connectors.scope("organization", "org_123");
const response = await scoped.connect("bank", { redirectUri: "https://app.com/callback" });
`$3
`typescript
const catalog = await connectors.list({ count: { connections: true } });
const mailchimp = await connectors.get("mailchimp", { count: { connections: true } });
`$3
`typescript
const list = await scoped.list({ where: { name: "Slack" } });
`$3
`typescript
const instance = await scoped.get("stripe");
const secret = instance?.config?.webhook?.secret as string | undefined;
const webhookUrl = secret
? IgniterConnectorUrl.buildWebhookUrl("stripe", secret)
: null;
`$3
`typescript
process.env.IGNITER_BASE_URL = "https://app.example.com";
process.env.IGNITER_BASE_PATH = "/api/v1";
const callbackUrl = IgniterConnectorUrl.buildOAuthCallbackUrl("stripe");
`$3
`typescript
const schema = z.object({ id: z.string() });
const valid = await IgniterConnectorSchema.validateOrThrow(schema, { id: "abc" });
`$3
`typescript
const schema = z.object({
apiKey: z.string().describe("API key"),
region: z.enum(["us", "eu"]).describe("Region"),
});
const fields = IgniterConnectorFields.fromSchema(schema);
`$3
`typescript
const fields = IgniterConnectorFields.fromSchema(schema);
const withValues = IgniterConnectorFields.mergeWithConfig(fields, {
apiKey: "sk_123",
region: "us",
});
`$3
`typescript
const masked = IgniterConnectorCrypto.maskFields(
{ apiKey: "sk_test_123456" },
["apiKey"],
"*",
3,
);
`$3
`typescript
const user = IgniterConnectorOAuthUtils.parseUserInfo({
sub: "user_123",
name: "Ava",
email: "ava@example.com",
});
`$3
`typescript
const canRefresh = IgniterConnectorOAuthUtils.canRefresh({
accessToken: "token",
refreshToken: "refresh",
});
`$3
`typescript
type Scoped = $InferScoped;async function sendAlert(scoped: Scoped, message: string) {
await scoped.action("slack", "postMessage").call({ text: message });
}
`$3
`typescript
type SlackConfig = $InferConfig;
const config: SlackConfig = { webhookUrl: "https://...", channel: "#ops" };
`$3
`typescript
type SlackActions = $InferActionKeys;
const action: SlackActions = "postMessage";
`$3
`typescript
connectors.on((event) => {
if (event.type === "connector.connected") {
console.log("Connected:", event.connector);
}
});
`$3
`typescript
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",
});
`$3
`typescript
const stats = await connectors.list({ count: { connections: true } });
`$3
`typescript
const { data } = await scoped.action("bank", "accounts").call({});
`$3
`typescript
const adapter = IgniterConnectorMockAdapter.create();
await adapter.save("org", "id", "slack", {}, true);
console.log(adapter.calls.save); // 1
`$3
`typescript
adapter.clear();
`$3
`typescript
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",
},
);
`$3
`typescript
const state = IgniterConnectorOAuthUtils.generateState();
`$3
`typescript
const verifier = IgniterConnectorOAuthUtils.generateCodeVerifier();
const challenge = await IgniterConnectorOAuthUtils.generateCodeChallenge(verifier);
`$3
`typescript
const encrypted = await IgniterConnectorCrypto.encryptFields(
{ token: "secret", name: "Test" },
["token"],
);
const decrypted = await IgniterConnectorCrypto.decryptFields(encrypted, ["token"]);
`$3
`typescript
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();
`$3
`typescript
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("organization", { required: true })
.addConnector("slack", slack)
.addConnector("telegram", telegram)
.addConnector("stripe", stripe)
.build();
`$3
`typescript
await adapter.update("organization", "org_123", "slack", { enabled: false });
`$3
`typescript
try {
await connectors.handle("webhook", request);
} catch (error) {
console.error("Webhook failed:", error);
}
`$3
`typescript
const list = await connectors.list();
const uiItems = list.map((item) => ({
key: item.key,
type: item.type,
name: item.metadata.name,
}));
`$3
`typescript
const instance = await scoped.get("slack");
const enabled = instance?.enabled ?? false;
`---
🧾 Full API Reference (Detailed)
$3
####
scope(scopeKey, identity?)
- Validates scope key
- Requires identity if scope is required: true
- Returns IgniterConnectorScoped`typescript
const scoped = connectors.scope("organization", "org_123");
`####
list(options?)
- Returns metadata for all registered connectors
- Optional count.connections includes connection counts`typescript
const list = await connectors.list({ limit: 10, offset: 0, count: { connections: true } });
`####
get(connectorKey, options?)
- Returns connector metadata or null`typescript
const info = await connectors.get("slack", { count: { connections: true } });
`####
action(connectorKey, actionKey)
- Uses defaultConfig from connector definition
- Returns { data, error }`typescript
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/callback or /api/v1/connectors/:key/webhook/:secret`typescript
return connectors.handle("oauth.callback", request);
`####
emit(event)
- Emits to internal handlers
- Emits telemetry when configured`typescript
await connectors.emit({
type: "connector.connected",
connector: "slack",
scope: "organization",
identity: "org_123",
timestamp: new Date(),
});
`$3
####
connect(connectorKey, config)
- Validates config schema (custom connectors)
- For OAuth connectors, uses redirectUri and returns Response`typescript
await scoped.connect("slack", { webhookUrl: "https://...", channel: "#alerts" });
`####
disconnect(connectorKey)`typescript
await scoped.disconnect("slack");
`####
toggle(connectorKey, enabled?)`typescript
await scoped.toggle("slack", false);
`####
action(connectorKey, actionKey)`typescript
await scoped.action("slack", "postMessage").call({ text: "Hello" });
`$3
####
withConfig(schema)
- Uses StandardSchemaV1 (Zod-compatible)`typescript
IgniterConnector.create().withConfig(z.object({ apiKey: z.string() }));
`####
withOAuth(options)
- Configures OAuth workflow`typescript
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.
$3
- 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()$3
- Context: Action attempted without prior connect
- Cause: No stored record for scope + connector
- Mitigation: Call .connect() first
- Solution: Ensure scoped.connect() completed$3
- Context: Attempted to connect twice
- Cause: Record already exists
- Mitigation: Check scoped.get() before connect
- Solution: Update instead of connect$3
- Context: Config schema validation failed
- Cause: Missing or invalid config fields
- Mitigation: Validate config client-side
- Solution: Fix config to match schema$3
- Context: Manager .action() used without defaultConfig
- Cause: No withDefaultConfig() in connector definition
- Mitigation: Use scoped actions
- Solution: Add withDefaultConfig()$3
- Context: Action key not registered
- Cause: Typo or missing action definition
- Mitigation: Use $InferActionKeys for type safety
- Solution: Add .addAction() or correct key$3
- Context: Action input schema failed
- Cause: Wrong input type or missing fields
- Mitigation: Validate input before calling
- Solution: Fix input to match schema$3
- Context: Action output schema mismatch
- Cause: Handler returns wrong shape
- Mitigation: Align handler output with schema
- Solution: Fix handler return type$3
- Context: Action handler threw
- Cause: API failure or unexpected exception
- Mitigation: Add error handling in handler
- Solution: Catch/transform errors$3
- Context: Unknown scope key
- Cause: Calling .scope() with unregistered scope
- Mitigation: Use $InferScopeKey
- Solution: Add .addScope()$3
- Context: Missing identity for required scope
- Cause: required: true scope but identity not provided
- Mitigation: Provide identity
- Solution: Pass identity or make scope optional$3
- Context: Database adapter missing
- Cause: .withDatabase() not called
- Mitigation: Ensure adapter is configured
- Solution: Add .withDatabase()$3
- Context: Adapter error
- Cause: DB connectivity or adapter bug
- Mitigation: Wrap adapter calls with retries
- Solution: Fix adapter implementation$3
- Context: OAuth operation on non-OAuth connector
- Cause: Missing .withOAuth()
- Mitigation: Verify connector definition
- Solution: Add .withOAuth()$3
- Context: OAuth state mismatch
- Cause: Cookies dropped or incorrect state
- Mitigation: Preserve cookies
- Solution: Retry OAuth flow$3
- Context: Token exchange failure
- Cause: Invalid credentials or provider error
- Mitigation: Check client ID/secret
- Solution: Fix OAuth credentials$3
- Context: Token response format unsupported
- Cause: Provider response is non-standard
- Mitigation: Provide parseTokenResponse
- Solution: Implement custom parser$3
- Context: User info response format unsupported
- Cause: Provider response is non-standard
- Mitigation: Provide parseUserInfo
- Solution: Implement custom parser$3
- Context: Refresh token invalid
- Cause: Token revoked or expired
- Mitigation: Reconnect via OAuth
- Solution: Start new OAuth flow$3
- Context: Webhook handler called without webhook config
- Cause: Missing .withWebhook()
- Mitigation: Add webhook config
- Solution: Use .withWebhook()$3
- Context: Webhook payload invalid
- Cause: Payload does not match schema
- Mitigation: Validate payload before sending
- Solution: Fix sender or schema$3
- Context: Signature verification failed
- Cause: Wrong secret or signature
- Mitigation: Ensure correct secret
- Solution: Fix verification logic$3
- Context: Encryption failure
- Cause: Invalid secret or custom encrypt error
- Mitigation: Validate encryption keys
- Solution: Fix IGNITER_SECRET or custom encrypt$3
- Context: Decryption failure
- Cause: Invalid secret or corrupted data
- Mitigation: Validate encryption keys
- Solution: Fix IGNITER_SECRET or custom decrypt$3
- Context: Missing IGNITER_SECRET
- Cause: Encryption attempted without secret
- Mitigation: Set env variable
- Solution: Provide secret or custom encrypt/decrypt$3
- Context: withConfig() missing
- Cause: Connector definition incomplete
- Mitigation: Always call .withConfig()
- Solution: Add config schema$3
- Context: No scopes configured
- Cause: Builder missing .addScope()
- Mitigation: Define at least one scope
- Solution: Add a scope$3
- 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.
$3
- igniter.connectors.connector.connected
- igniter.connectors.connector.disconnected
- igniter.connectors.connector.enabled
- igniter.connectors.connector.disabled
- igniter.connectors.connector.updated$3
- igniter.connectors.oauth.started
- igniter.connectors.oauth.completed
- igniter.connectors.oauth.refreshed
- igniter.connectors.oauth.failed$3
- igniter.connectors.action.started
- igniter.connectors.action.completed
- igniter.connectors.action.failed$3
- igniter.connectors.webhook.received
- igniter.connectors.webhook.processed
- igniter.connectors.webhook.failed$3
- igniter.connectors.adapter.get
- igniter.connectors.adapter.list
- igniter.connectors.adapter.upsert
- igniter.connectors.adapter.update
- igniter.connectors.adapter.delete$3
- 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
$3
`typescript
const fields = IgniterConnectorFields.fromSchema(connector.configSchema);
const formSchema = fields.map((field) => ({
label: field.label,
name: field.key,
required: field.required,
}));
`$3
`typescript
async function sendBatch(scoped: $InferScoped, messages: string[]) {
for (const message of messages) {
await scoped.action("slack", "postMessage").call({ text: message });
}
}
`$3
`typescript
class CachedAdapter extends IgniterConnectorBaseAdapter {
private cache = new Map(); 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)
$3
`typescript
await connectors.scope("organization", "org_999")
.connect("telegram", { botToken: "token", chatId: "123" });
`$3
`typescript
await connectors.scope("organization", "org_999")
.disconnect("telegram");
`$3
`typescript
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();
`$3
`typescript
const { data, error } = await scoped.action("crm", "createContact").call({
email: "invalid",
});
if (error) {
console.error("Action failed:", error.message);
}
`$3
`typescript
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();
`$3
`typescript
connectors.on((event) => {
console.log([${event.type}] ${event.connector});
});
`$3
`typescript
scoped.on((event) => {
console.log([${event.type}] ${event.connector});
});
`$3
`typescript
const parsed = IgniterConnectorUrl.parseWebhookUrl(
"https://app.example.com/api/v1/connectors/stripe/webhook/secret123",
);
`$3
`typescript
const parsed = IgniterConnectorUrl.parseOAuthCallbackUrl(
"https://app.example.com/api/v1/connectors/stripe/oauth/callback?code=123",
);
`$3
`typescript
const isSchema = IgniterConnectorSchema.isSchema(z.string());
`$3
`typescript
const scopes = connectors.getScopes();
`$3
`typescript
const defs = connectors.getConnectors();
`$3
`typescript
const list = await connectors.list({ count: { connections: true } });
const metrics = list.map((item) => ({
connector: item.key,
connections: item.connections ?? 0,
}));
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
if (policy.shouldDisable(connectorKey)) {
await scoped.toggle(connectorKey, false);
}
`$3
`typescript
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();
`$3
`typescript
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();
`$3
`typescript
try {
await scoped.connect("slack", { webhookUrl: "not-a-url", channel: "#general" });
} catch (error) {
if (error instanceof IgniterConnectorError) {
console.error(error.code, error.metadata);
}
}
`$3
`typescript
const tokens = IgniterConnectorOAuthUtils.parseTokenResponse({
token: "token",
expires: 3600,
});
`$3
`typescript
const result = await IgniterConnectorSchema.validate(schema, input);
if (!result.success) {
console.error(result.errors);
}
`$3
`typescript
await scoped.action("slack", "postMessage").call({ text: "Deploy done" });
`$3
`typescript
const schema = z.object({
apiKey: z.string().min(1).max(64),
});
`$3
`typescript
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("system", { required: false })
.addConnector("status", systemConnector)
.build();const system = manager.scope("system");
`$3
`typescript
const instance = await scoped.get("slack");
if (!instance?.enabled) {
throw new Error("Connector disabled");
}
`$3
`typescript
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();
`$3
`typescript
const webhookOnly = IgniterConnector.create()
.withConfig(z.object({ webhookSecret: z.string() }))
.withWebhook({
schema: z.object({ event: z.string() }),
handler: async ({ payload }) => ({ ok: payload.event }),
})
.build();
`$3
`typescript
const manager = IgniterConnectorManager.create()
.withDatabase(adapter)
.addScope("workspace", { required: true })
.addConnector("slack", slack)
.build();const scoped = manager.scope("workspace", "wk_123");
`$3
`typescript
const isEncrypted = IgniterConnectorCrypto.isEncrypted("iv:tag:cipher");
`$3
`typescript
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.
$3
- ctx.connector.provider
- ctx.connector.scope
- ctx.connector.identity$3
- ctx.connector.encrypted
- ctx.connector.encryptedFields$3
- ctx.oauth.authorizationUrl
- ctx.oauth.tokenUrl
- ctx.oauth.pkce
- ctx.oauth.scopes
- ctx.oauth.hasState$3
- ctx.action.name
- ctx.action.durationMs
- ctx.action.success$3
- ctx.webhook.method
- ctx.webhook.path
- ctx.webhook.durationMs
- ctx.webhook.verified$3
- ctx.error.code
- ctx.error.message
- ctx.error.operation
- ctx.error.action$3
- ctx.adapter.durationMs
- ctx.adapter.found
- ctx.adapter.count
- ctx.adapter.inserted---
❓ FAQ
$3
No. You can use any StandardSchemaV1 compatible library. Zod is the most common.$3
OAuth connect uses redirectUri only. Use withDefaultConfig() for static config values.$3
No. The package is server-only and ships a browser shim that throws explicit errors.$3
When you call .withTelemetry() on the manager.$3
Yes. Skip withEncrypt() and do not access crypto utilities.$3
Use .withWebhook({ schema, verify }) to validate payload and verify signatures.$3
Yes for scoped operations. Manager .action() can run without database using defaultConfig.$3
Use IgniterConnectorMockAdapter.$3
Yes. Scopes are arbitrary keys.$3
Yes. It refreshes tokens when expired and refreshToken is available.$3
Yes, use parseTokenResponse.$3
Yes, use parseUserInfo.$3
Use .onError() and/or .on() event handlers.$3
Yes. Use .emit() with IgniterConnectorEvent shape.$3
Use IgniterConnectorUrl.buildWebhookUrl() and the stored webhook.secret.$3
It defines the base URL for OAuth and webhook URLs.$3
The package uses node:crypto in IgniterConnectorCrypto.$3
Use connectors.list().$3
Use scoped.list().$3
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
- @igniter-js/telemetry
- @igniter-js/common
---
📜 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
`bash
npm
npm install @igniter-js/connectors @igniter-js/commonpnpm
pnpm add @igniter-js/connectors @igniter-js/commonyarn
yarn add @igniter-js/connectors @igniter-js/commonbun
bun add @igniter-js/connectors @igniter-js/common
`Quick Start
$3
Use the
Connector builder to define what a connector needs and can do:`typescript
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://