Skip to content

Connector SDK

@bainquet/connector-sdk is the reference bAInquet connector: a typed, dependency-free TypeScript client over the five ingestion endpoints. Every other connector and the Universal AI Plugin pipeline target it, and it is the conformance reference.

Stable Version 0.3.0. Signing is verified byte-for-byte against the server's request verifier.

The SDK is dependency-free at runtime: signing uses node:crypto, and HTTP uses the global fetch (Node 18 or newer).

Install

bash
npm install @bainquet/connector-sdk

You issue a connector token in the dashboard for a verified website. You receive a connectorId.secret token exactly once; store it then.

createBainquetConnector

ts
import { createBainquetConnector, makeItem } from "@bainquet/connector-sdk";

const connector = createBainquetConnector({
  baseUrl: "https://api.bainquet.online",
  token: process.env.BAINQUET_CONNECTOR_TOKEN!, // "connectorId.secret"
  websiteId: "site_123",
  siteDomain: "example.com",
  connectorType: "api", // X-Connector-Type
});

const item = makeItem({
  type: "product",
  id: "p_1",
  url: "https://example.com/product/a",
  title: "Product A",
  html: "<h1>Product A</h1>",
  language: "en",
});

const result = await connector.ingestBatch([item]);
console.log(result.accepted, "accepted,", result.skipped, "skipped");

Options

OptionRequiredDefaultPurpose
baseUrlyesAPI origin, for example https://api.bainquet.online. A trailing slash is trimmed.
tokenyesconnectorId.secret issued by the dashboard.
websiteIdyesThe website the connector is bound to (the canonical signing field and HKDF salt).
siteDomainyesX-Site-Domain; must match each item's url host (or a verified alias).
connectorTypeno"api"X-Connector-Type.
connectorVersionnothe SDK versionX-Connector-Version.
maxRetriesno3Max retry attempts on 5xx / 429 / network error.
backoffBaseMsno500Base backoff for exponential plus jitter.
backoffCapMsno30000Backoff cap.
fetchImplnoglobal fetchfetch override (for testing).
sleepImplnosetTimeoutSleep override (for testing).

Client methods

Each method serializes the body once, signs those exact bytes, POSTs with the full header set, and returns the envelope data on success. On an envelope error (ok: false) it throws a typed BainquetError. The per-call opts accepts { partial?, idempotencyKey? }.

ingestBatch

ts
ingestBatch(items: IngestItem[], opts?: IngestOpts): Promise<IngestBatchResult>

POSTs { website_id, items, partial } to POST /v1/ingest/batch (1 to 500 items). partial defaults to true, so a bad item is isolated as a per-item error while good items are still accepted.

ts
const res = await connector.ingestBatch(items);
// res: { received, accepted, skipped, errored, results: [{ id, status, ... }] }
for (const r of res.results) {
  if (r.status === "error") console.error(r.id, r.error?.code);
}

ingestItem

ts
ingestItem(item: IngestItem, opts?: IngestOpts): Promise<IngestItemResult>

POSTs { website_id, item } to POST /v1/ingest/item for a single upsert.

delete

ts
delete(ids: string[], reason?: DeleteReason, opts?: IngestOpts): Promise<IngestDeleteResult>

POSTs { website_id, ids, reason } to POST /v1/ingest/delete to tombstone items by stable id. reason is one of source_deleted, gdpr_erasure, connector_resync, takedown. The result reports tombstoned[], not_found[], and a republish_job_id.

heartbeat

ts
heartbeat(
  input?: {
    cursor?: string;
    stats?: { items_seen?: number; items_pending?: number; last_error?: string };
    connectorState?: ConnectorState;
  },
  opts?: IngestOpts,
): Promise<IngestHeartbeatResult>

POSTs to POST /v1/ingest/heartbeat. Report your last processed cursor so a resumed run continues where it left off. Honor the response next_action (continue, pause, or backfill) and use the server time to correct local clock drift before signing.

advertiseSchema

ts
advertiseSchema(mappings: FieldMapping[], opts?: IngestOpts): Promise<IngestSchemaResult>

POSTs { website_id, mappings } to POST /v1/ingest/schema to tell the server how to interpret custom attributes. Each mapping has a source_path, a target, an attribute_name when target is attribute, and a provenance_method.

makeItem and makeChecksum

ts
import { makeItem, makeChecksum } from "@bainquet/connector-sdk";

makeChecksum({ type, url, html?, text?, json? }) computes the deterministic sha256:<hex> content checksum by deeply sorting object keys, treating an absent field as null, and hashing the canonicalized JSON. Identical logical content yields an identical checksum, so re-posting unchanged content always dedupes to skipped. The checksum deliberately excludes updated_at.

makeItem({ type, id, url, title?, html?, text?, json?, language?, canonicalUrl?, updatedAt?, attributes? }) builds an IngestItem with a correct checksum. Supply at least one of html, text, or json. Use makeChecksum directly if you assemble items yourself.

Idempotency

Two layers:

  • Request-level: every call carries an Idempotency-Key (a fresh uuid unless you pass opts.idempotencyKey). A replay with the same key returns the original response and enqueues no duplicate work. The same key with a different body is a 409 ingest.duplicate.
  • Item-level: a (website_id, id, checksum) already seen returns status: "skipped" with no re-enqueue.

On a 5xx / 429 / network failure the SDK retries with exponential backoff plus jitter (honoring Retry-After), reusing the same Idempotency-Key but signing each attempt with a fresh nonce and timestamp.

Signing primitives

For a custom-language connector, the signing primitives are exported from @bainquet/connector-sdk/sign so you can build the scheme yourself or validate another implementation against this reference:

ExportSignaturePurpose
parseToken(raw) => { connectorId, secret } | nullSplit the wire token on the first ..
deriveSigningKey(secret, websiteId) => BufferHKDF-SHA256(secret, salt=websiteId, info="bq.connector.hmac.v1", 32).
sha256Hex(input) => stringLowercase-hex SHA-256, the bodySha256 field.
canonicalString(parts) => stringThe six newline-joined canonical fields.
computeSignature(signingKey, canonical) => stringHex HMAC-SHA256 over the canonical string.
signRequest({ token, websiteId, method?, path, body, now?, nonce? }) => SignedRequestThe full one-call signer returning headers plus the exact body to send.
ts
import { signRequest } from "@bainquet/connector-sdk/sign";

const body = JSON.stringify({ website_id: "site_123", items: [item] });
const signed = signRequest({
  token: process.env.BAINQUET_CONNECTOR_TOKEN!,
  websiteId: "site_123",
  path: "/v1/ingest/batch", // pathname only: no host, no query
  body,
});
// signed.headers has Authorization, X-Signature, X-Timestamp, X-Nonce,
// X-Body-Sha256, Content-Type. Add X-Site-Domain, X-Connector-Type/Version,
// and Idempotency-Key yourself, then POST signed.body unchanged.

WARNING

Sign the exact bytes you send. signRequest returns the body it signed; POST that string verbatim. Re-serializing the body after signing changes its sha256 and produces a 401 auth.invalid_signature.

For the full scheme and a raw worked example in any language, see Generic API connector and Ingestion and signing.

CLI: bainquet-ingest

bainquet-ingest is a thin demonstration of the SDK: it reads a JSON file of items and posts them as one batch.

bash
bainquet-ingest \
  --base    https://api.bainquet.online \
  --token   "$BAINQUET_CONNECTOR_TOKEN" \
  --website site_123 \
  --domain  example.com \
  --file    items.json

items.json is either a JSON array of items or { "items": [ ... ] }. Any item without a checksum gets one computed for you. The command prints a received / accepted / skipped / errored summary and exits non-zero if any item errored or the request failed.

Error handling

Every method throws a typed BainquetError on an envelope error:

ts
import { BainquetError } from "@bainquet/connector-sdk";

try {
  await connector.ingestBatch(items);
} catch (err) {
  if (err instanceof BainquetError) {
    console.error(err.code, err.status, err.requestId, err.retryable);
    if (err.retryable) {
      // safe to retry the same idempotent operation
    }
  }
}

BainquetError carries code (the registry error code), status (HTTP), requestId, details, and retryable. Branch on code, not the HTTP status alone; see Envelope and errors.

Owner-controlled structured data for AI.