npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@igniter-js/connectors

v0.1.11

Published

Type-safe, multi-tenant connector management library for Igniter.js with OAuth, encryption, and adapter support

Readme

@igniter-js/connectors

npm version License: MIT TypeScript Node.js

Type-safe connector management for multi-tenant integrations
Build OAuth, API-key, and webhook connectors with strong typing, encryption, and telemetry.

Quick StartCore ConceptsExamplesAPI ReferenceTelemetry


✨ 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 zod

60-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 IgniterConnectorsTelemetryEvents to 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); // 1

Example 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.connections includes 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 defaultConfig from 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/callback or /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 redirectUri and returns Response
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 $InferActionKeys for 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: true scope 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_SECRET or custom encrypt

CONNECTOR_DECRYPT_FAILED

  • Context: Decryption failure
  • Cause: Invalid secret or corrupted data
  • Mitigation: Validate encryption keys
  • Solution: Fix IGNITER_SECRET or 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.connected
  • igniter.connectors.connector.disconnected
  • igniter.connectors.connector.enabled
  • igniter.connectors.connector.disabled
  • igniter.connectors.connector.updated

OAuth Flow

  • igniter.connectors.oauth.started
  • igniter.connectors.oauth.completed
  • igniter.connectors.oauth.refreshed
  • igniter.connectors.oauth.failed

Action Execution

  • igniter.connectors.action.started
  • igniter.connectors.action.completed
  • igniter.connectors.action.failed

Webhooks

  • igniter.connectors.webhook.received
  • igniter.connectors.webhook.processed
  • igniter.connectors.webhook.failed

Adapter

  • igniter.connectors.adapter.get
  • igniter.connectors.adapter.list
  • igniter.connectors.adapter.upsert
  • igniter.connectors.adapter.update
  • igniter.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.provider
  • ctx.connector.scope
  • ctx.connector.identity

Connection Attributes

  • ctx.connector.encrypted
  • ctx.connector.encryptedFields

OAuth Attributes

  • ctx.oauth.authorizationUrl
  • ctx.oauth.tokenUrl
  • ctx.oauth.pkce
  • ctx.oauth.scopes
  • ctx.oauth.hasState

Action Attributes

  • ctx.action.name
  • ctx.action.durationMs
  • ctx.action.success

Webhook Attributes

  • ctx.webhook.method
  • ctx.webhook.path
  • ctx.webhook.durationMs
  • ctx.webhook.verified

Error Attributes

  • ctx.error.code
  • ctx.error.message
  • ctx.error.operation
  • ctx.error.action

Adapter Attributes

  • ctx.adapter.durationMs
  • ctx.adapter.found
  • ctx.adapter.count
  • ctx.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

NPM Version License: MIT

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/common

Quick 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://