plugline
v0.2.0
Published
Vendor-agnostic integration SDK: declarative connectors, pluggable auth strategies, OAuth 2.0 authorization-code flow, retries and rate-limit handling, and a host adapter interface. Zero runtime dependencies.
Maintainers
Readme
plugline
Vendor-agnostic integration SDK for production TypeScript backends. Declarative connectors, pluggable auth strategies, OAuth 2.0, retries, rate-limit handling, webhooks. Zero runtime dependencies.
bun add plugline
npm install pluglineWhy plugline
A TMS that integrates with 100 vendors eventually notices the same pattern: every vendor has a different auth scheme, a different error shape, a different pagination convention, and the connector that talks to them is mostly boilerplate around that. plugline is the framework you only write once.
A third-party TMS that wants to reuse your connectors on their own models can
also bun add plugline, write a bind function against their own schema, and
inherit the same auth, refresh, retry and rate-limit behaviour for free.
Minimal example
import {
defineConnector,
apiKey,
Registry,
type IntegrationHost,
} from "plugline";
// 1. Tell the SDK how to talk to your app (database, env, clock, fetch).
const host: IntegrationHost = {
fetch: (input, init) => fetch(input, init),
now: () => Date.now(),
env: (key) => process.env[key],
saveSecrets: async () => {},
loadSecrets: async () => null,
upsertIntegration: async () => ({ integrationId: "int_1" }),
getIntegration: async () => null,
recordRun: async () => {},
markRun: async () => {},
};
// 2. Declare a manifest and a connector.
const acme = defineConnector<{ id: string; lat: number; lng: number }>({
manifest: {
id: "acme",
name: "Acme Tracking",
provider: "Acme Inc.",
tagline: "Live trailer positions",
description: "Reads trailer locations from the Acme telematics API.",
category: "ELD & Telematics",
docsUrl: "https://developer.acme.example",
authType: "apiKey",
credentialFields: [
{
key: "api_key",
label: "Acme API Key",
type: "password",
required: true,
},
],
syncEntities: [
{
id: "gps",
label: "GPS",
description: "Live positions",
defaultEnabled: true,
},
],
syncCadence: "every_15_mins",
features: ["Live GPS"],
},
auth: apiKey({ headerName: "X-Acme-Key" }),
baseUrl: "https://api.acme.example",
itemsPath: "data.trailers",
probe: { path: "/v1/trailers", query: { limit: 1 } },
list: { path: "/v1/trailers" },
paginate: {
cursorParam: "cursor",
nextCursorPath: "meta.next_cursor",
hasMorePath: "meta.has_more",
},
mapping: [
{ target: "id", source: "id" },
{ target: "lat", source: "position.lat" },
{ target: "lng", source: "position.lng" },
],
// 3. Tell the SDK how to write records to your models. Everything else is automatic.
bind: async (ctx, records) => {
// ctx.tenantId scopes every call. records is your normalised shape.
return { persisted: records.length };
},
});
// 4. Register. A card can only exist in your settings UI for connectors
// that are actually registered — the catalog is derived from the registry.
const registry = new Registry();
registry.register(acme);What the SDK owns
- Auth strategies:
apiKey,basicAuth,bearerToken,oauth2AuthCode - OAuth 2.0 authorization code flow: CSRF state with HMAC, code exchange, just-in-time refresh, re-encrypted token persistence
- Vendor error parsing: Google, RFC 6749, Samsara, and plain-text bodies
- Request loop: timeouts, retries on 5xx + 429 with
Retry-Afterhonoured, 4xx passed through - Pagination: cursor-based, with
hasMoreflag and max-page guard - Mapping: dot-path extraction with transforms and fallbacks
- Webhooks: HMAC-SHA256 verification, idempotent dispatch
- Sync logging: every connector call produces an audit row, success or failure
- Run measurements: latency is always measured, never invented
What the host owns
- Persistence (database, encryption, multi-tenant scoping)
- The
fetchtransport (and any caching or tracing you wrap around it) - The clock (so tests can be deterministic)
env(key)for server-side secrets- One
bindper connector that maps normalised records to your models
The boundary is one interface (IntegrationHost). The SDK never imports
your database, your session, or your ORM.
Built-in recipes
| Recipe | Status | Notes |
| ------------------------- | ------ | ------------------------------------------------------------------------- |
| googleMapsRecipe | live | verified against routes.googleapis.com and the Geocoding API |
| samsaraRecipe | live | /fleet/vehicles/locations (50 req/s), real Dallas→Atlanta mileage check |
| samsaraHosRecipe | live | /fleet/hos/clocks (5 req/s) for compliance-aware dispatch |
| samsaraViolationsRecipe | live | /fleet/hos/violations for active compliance violations |
Each recipe verifies against the live vendor API in scripts/test-sdk.ts.
API surface
- Types —
IntegrationManifest,IntegrationConnector,IntegrationContext,IntegrationHost,OAuthConfig,RecipeOptions,EndpointSpec,FieldMapping - Strategies —
apiKey,basicAuth,bearerToken,oauth2AuthCode - Engine —
defineConnector,Registry - Helpers —
validate,validateManifest,assertCredentialsPresent,parseDurationSeconds,parseVendorError,redact,maskSecret,metersToMiles,formatLocation - Runtime —
fetchWithRetry,parseRateLimitHeaders,handleWebhook,readWebhook,verifyHmacSha256
Running the tests
bun run test # 114+ assertions, all offline, no network
bun run typecheck # tsc, 0 errors
bun run build # bundles dist/The tests use a mock host — they prove the SDK never silently fabricates a success, surfaces real vendor errors verbatim, and survives 5xx, 4xx, 429 and network errors with the right policy each time.
License
MIT
