@nightowlsdev/integration-native
Adapter/ToolingThe fully-open DEFAULT IntegrationProvider backend, self-run OAuth, an authed auto-refreshing proxy, provider-direct webhooks, and a minimal poller, with credentials on your own infrastructure (no vendor catalog, no Nango).
What it does
@nightowlsdev/integration-native is the batteries-included, zero-vendor `IntegrationProvider` backend (the seam lives in `@nightowlsdev/connectors`). It turns hand-authored `defineConnector` defs into a working backend with credentials on YOUR infrastructure. `nativeProvider(opts)` implements: self-run OAuth (builds the PKCE + `state` authorization URL, exchanges the code, stores tokens in an injected `TokenVault`; OAuth2 uses the connector's inline endpoints, OIDC auto-discovers from the issuer, `oauth4webapi` is confined to this package so the engine wall holds); an authed proxy (`executeTool` calls a connector's declarative `op` with the vaulted token, auto-refreshing on known expiry and reactively on a 401, then fencing output, credential failures raise `ConnectorConnectionError` → re-auth, `429/502/503/504` raise `ConnectorRetryableError` → back off); provider-direct webhooks (`handleInboundWebhook` verifies the provider's own signature against a rotation-aware `WebhookSecretStore`, normalizes via the connector's event spec, returns a `TriggerEvent`); and `createNativePoller`, a host-scheduled (cron / Trigger.dev, no in-engine timers) driver that polls webhook-less providers a page at a time into CN8's `handleSyncEvent`, fail-safe and cursor-advancing only after delivery. Everything stateful, vault, connection persistence, per-provider OAuth app creds, the single-use PKCE/state store, the webhook verify scheme, the cursor store, is injected, so the package stays hermetically unit-testable and the host owns its storage. Engine-wall clean: peer-depends only on `@nightowlsdev/connectors`.
Install
pnpm add @nightowlsdev/integration-nativeKey exports
- nativeProvider
- createNativePoller
- types: NativeProviderOpts, NativePollerOpts, PollTarget, SyncCursorStore
Usage
import { nativeProvider, createNativePoller } from "@nightowlsdev/integration-native";
import { materializeIntegration } from "@nightowlsdev/connectors";
import { defineSwarm } from "@nightowlsdev/core";
// The fully-open default IntegrationProvider backend, self-run OAuth, credentials on YOUR infra.
const provider = nativeProvider({
connectors: [githubConnector, slackConnector], // your hand-authored defineConnector defs
vault, // @nightowlsdev/storage-supabase makeTokenVault, or your own TokenVault
persistConnection, // write the authoritative owl_connections row (backend: "native")
oauthClient, // (provider) => { clientId, clientSecret?, redirectUri } from env
stateStore, // single-use PKCE/state store
baseUrlFor: (p) => API_BASES[p],
webhook: { secrets, verify, parse }, // optional: provider-direct webhooks (rotation-aware)
});
// Grant the provider's tools to agents by skillNames (same seam as materializeConnectors).
const swarm = defineSwarm({ agents, connectorTools: materializeIntegration(provider, ctxToRefs) });
// Poll webhook-less providers on your own schedule (cron / Trigger.dev, no in-engine timers).
const poller = createNativePoller({ provider, targets, intake, cursors });What it provides
integration-native is the fully-open DEFAULT IntegrationProvider backend — no vendor catalog, no Nango, credentials on YOUR infrastructure. nativeProvider(opts) turns hand-authored defineConnector defs into a working backend: it runs the OAuth authorization-code flow itself (PKCE + state; OAuth2 uses the connector's inline endpoints, OIDC auto-discovers from the issuer), stores tokens in an injected TokenVault, proxies a connector's declarative op with the vaulted access token (auto-refresh on known expiry and reactively once on a 401), and verifies provider-direct webhooks against a rotation-aware secret store. createNativePoller drives webhook-less providers into the same CN8 trigger fan-out. oauth4webapi is confined to this package so the engine wall holds; every stateful dependency is injected.
When to use it
- You want an integration backend where OAuth tokens live on your own infrastructure — never a third-party cloud. This is the open, self-hosted default.
- You've authored providers as defineConnector defs and want them connectable (OAuth2 or OIDC) and proxied with per-tenant credentials.
- You need provider-direct webhooks — the provider POSTs you, you verify its own signature — with rotation-aware secrets (old + new both valid during a swap).
- A provider has no webhooks, so you poll it on your own schedule (cron / Trigger.dev) and feed records into the same trigger fan-out webhooks use.
When not to
- You'd rather not run OAuth apps, rotate secrets, or store tokens at all — a managed catalog like Composio (ownsCatalog) hands you tools + hosted credentials, at the cost of your users' tokens living in its cloud.
- You want a large pre-built catalog (1000+ apps) without hand-authoring each connector's actions — that is Composio's catalog, not native's hand-authored defs.
- The connector action is a custom execute rather than a declarative op — native's proxy only runs declarative HTTP ops; a custom execute runs via the materialized tool path.
Alternatives
- @nightowlsdev/integration-composioYou want a managed catalog + hosted credentials and are willing to point your users' tokens at Composio's cloud. Route the sensitive providers back to native with compositeProvider.
- staticBackend / nangoBackend (from @nightowlsdev/connectors)You only need the simpler materializeConnectors path, not the full IntegrationProvider seam — staticBackend for env/config creds (zero infra, the demo default), nangoBackend for per-org OAuth delegated to Nango.
Strengths
- Credentials on your infrastructure — the vaulted tokens never leave your storage, and nothing is metered by a vendor.
- Real OAuth done for you: PKCE + state, OAuth2 inline endpoints or OIDC discovery, proactive-on-expiry and reactive-on-401 refresh — all mapped through the P1 error model (dead credential → ConnectorConnectionError / re-auth; 429/502/503/504 → ConnectorRetryableError / back off).
- Engine-wall clean and hermetically testable — oauth4webapi is the only vendor dep and it is confined here; the vault, connection persistence, OAuth app creds, PKCE/state store, webhook verify, and cursor store are all injected.
- Provider-direct webhooks are rotation-aware and strictly route (trusted headers) → verify → parse → normalize, so an unverified body never drives normalization.
- The poller is fail-safe: one target's error never aborts the others, and a cursor advances only after its batch was delivered — a failure re-polls rather than skipping records.
Limits & trade-offs
- You own the operational burden: register OAuth apps per provider, rotate secrets, run the vault, schedule the poller. Composio hands you all of that.
- Only hand-authored defineConnector defs are connectable — there is no 1000-app catalog; you write each provider's actions and events.
- provider.executeTool proxies declarative op actions only; a connector action with a custom execute must run via the materialized tool path (materializeConnectors), which carries the full run/tool context.
- No in-engine timers — the poller is host-scheduled; you must wire cron / Trigger.dev yourself.
- Per-agent credential isolation (FR-058) requires a TokenVault that scopes by the executing ctx and echoes rowIdentity on resolved records; without refreshShared, concurrent same-row refreshes can race on a rotating-refresh-token provider.
How it works
nativeProvider(opts) indexes your connectors by provider and action name (throwing at construction on a collision so a call never routes to the wrong connector). startConnect builds a PKCE + state authorization URL and stashes the transaction in your single-use state store; completeConnect takes the state (atomic fetch-and-delete, so it can't replay), exchanges the code for tokens, vaults them, and writes the authoritative owl_connections row (backend "native"). executeTool validates the connection handle fail-closed, reads the executing agent's vaulted record, refreshes proactively on known expiry and reactively once on a 401, fetches the connector's op, and fences the output. handleInboundWebhook routes from trusted headers, verifies the raw body against rotation-aware secrets, then parses and normalizes. createNativePoller(...).poll() reads each target's cursor, fetches one page, feeds records to CN8's handleSyncEvent, and advances the cursor only on delivery.
Examples
Wire the native provider into a swarm
OAuth tokens stay in your vault; the provider proxies each connector op with the vaulted access token. ctxToRefs is your per-agent allow-list of connections.
import { nativeProvider } from "@nightowlsdev/integration-native";
import { materializeIntegration } from "@nightowlsdev/connectors";
import { defineSwarm } from "@nightowlsdev/core";
const provider = nativeProvider({
connectors: [githubConnector, slackConnector], // hand-authored defineConnector defs
vault, // TokenVault — storage-supabase makeTokenVault, or your own
persistConnection, // write the owl_connections row (backend: "native")
oauthClient, // (provider) => { clientId, clientSecret?, redirectUri }
stateStore, // single-use PKCE/state store
baseUrlFor: (p) => API_BASES[p],
});
// Same connectorTools seam materializeConnectors uses.
const swarm = defineSwarm({ agents, connectorTools: materializeIntegration(provider, ctxToRefs) });Complete the OAuth callback
native also exposes completeConnect (not part of the base seam) — it vaults the tokens and persists the connection as active.
// In your OAuth redirect route:
const { ref } = await provider.completeConnect({ code, state });
// ref = { tenantId, provider, userId? } — the credential is vaulted and the row is "active".Poll a webhook-less provider on your own schedule
No in-engine timers — you call poll() from cron / Trigger.dev; a cursor advances only after its batch was delivered.
import { createNativePoller } from "@nightowlsdev/integration-native";
const poller = createNativePoller({
provider: "github",
targets: [{ eventType: "issues", syncKey: "issues", externalIdOf: (r) => (r as { id: string }).id }],
cursorStore, // SyncCursorStore — persists the poll position per (provider, syncKey)
fetchPage: async (target, cursor) => fetchIssuesPage(cursor), // returns { records, nextCursor? }
intake, // PollerIntake — dedupe + lookupSubscriptions + enqueue (the CN8 fan-out)
});
const result = await poller.poll(); // fail-safe: always resolves, never throws out of the loopDoing the parts it doesn't support
- Storing credentials off your infrastructureThat is not native's job. If you want hosted credentials + a managed catalog, use @nightowlsdev/integration-composio, and route the providers you can't hand to a vendor back to native via compositeProvider.
- A connector action that isn't a declarative HTTP opnative's executeTool only proxies declarative op actions; a custom execute runs through the materialized tool path (materializeConnectors), which carries the full run/tool context executeTool can't reach.
- Scheduling the pollernative ships no timer. Wire poller.poll() to a cron / Trigger.dev schedule yourself (e.g. via runner-background), and alert off the per-target failed / throttled counts it returns.
- Per-agent credential rowsInjected — provide a TokenVault that scopes reads by the executing ctx and echoes rowIdentity on resolved records (the reference storage-supabase vault does, and offers refreshShared to serialize concurrent refreshes).
Related
- connectors — The IntegrationProvider seam + materializeIntegration this backend plugs into.
- integration-composio — The managed-catalog sibling backend; compose the two with compositeProvider.
- storage-supabase — The reference TokenVault (AES-256-GCM) + connection persistence.
- runner-background — Schedule the poller and run the durable runs webhook/poll triggers enqueue.
- knowledge-and-tools — The host surface that manages integration connections.