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
npm install @bainquet/connector-sdkYou issue a connector token in the dashboard for a verified website. You receive a connectorId.secret token exactly once; store it then.
createBainquetConnector
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
| Option | Required | Default | Purpose |
|---|---|---|---|
baseUrl | yes | — | API origin, for example https://api.bainquet.online. A trailing slash is trimmed. |
token | yes | — | connectorId.secret issued by the dashboard. |
websiteId | yes | — | The website the connector is bound to (the canonical signing field and HKDF salt). |
siteDomain | yes | — | X-Site-Domain; must match each item's url host (or a verified alias). |
connectorType | no | "api" | X-Connector-Type. |
connectorVersion | no | the SDK version | X-Connector-Version. |
maxRetries | no | 3 | Max retry attempts on 5xx / 429 / network error. |
backoffBaseMs | no | 500 | Base backoff for exponential plus jitter. |
backoffCapMs | no | 30000 | Backoff cap. |
fetchImpl | no | global fetch | fetch override (for testing). |
sleepImpl | no | setTimeout | Sleep 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
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.
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
ingestItem(item: IngestItem, opts?: IngestOpts): Promise<IngestItemResult>POSTs { website_id, item } to POST /v1/ingest/item for a single upsert.
delete
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
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
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
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 passopts.idempotencyKey). A replay with the same key returns the original response and enqueues no duplicate work. The same key with a different body is a409 ingest.duplicate. - Item-level: a
(website_id, id, checksum)already seen returnsstatus: "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:
| Export | Signature | Purpose |
|---|---|---|
parseToken | (raw) => { connectorId, secret } | null | Split the wire token on the first .. |
deriveSigningKey | (secret, websiteId) => Buffer | HKDF-SHA256(secret, salt=websiteId, info="bq.connector.hmac.v1", 32). |
sha256Hex | (input) => string | Lowercase-hex SHA-256, the bodySha256 field. |
canonicalString | (parts) => string | The six newline-joined canonical fields. |
computeSignature | (signingKey, canonical) => string | Hex HMAC-SHA256 over the canonical string. |
signRequest | ({ token, websiteId, method?, path, body, now?, nonce? }) => SignedRequest | The full one-call signer returning headers plus the exact body to send. |
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.
bainquet-ingest \
--base https://api.bainquet.online \
--token "$BAINQUET_CONNECTOR_TOKEN" \
--website site_123 \
--domain example.com \
--file items.jsonitems.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:
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.