@facet-llc/client
v0.2.1
Published
Facet Terminal client SDK — Node / Bun / Deno. Bundles the KYA token handling, error envelope parsing, trace-id propagation, and rate-limit awareness that every agent needs to talk to a Facet Terminal.
Readme
@facet/client
Client SDK for the Facet Terminal. Speaks the wire contract defined by @facet/protocol — KYA bearer token handling, structured error envelopes, X-Agent-Trace-Id propagation, X-Facet-RateLimit-* capture, and idempotency-key forwarding — so agent operators can plug into any Facet Terminal without hand-rolling the boilerplate.
Runs on Node 20+, Bun, and Deno. Uses native fetch + AbortController on all three.
Install
npm install @facet/client @facet/protocol
# or
pnpm add @facet/client @facet/protocol
# or
bun add @facet/client @facet/protocolBoth packages ship together — @facet/client declares @facet/protocol as
a runtime dependency, so package managers pull it in automatically.
Quick start
import { FacetClient, FacetClientError } from "@facet/client";
const client = new FacetClient({
terminalUrl: "https://facet.acme-ingredients.com/v1",
kyaToken: "eyJhbGciOiJFUzI1NiIs...", // or a function returning a fresh token
});
// Discovery (no auth)
const caps = await client.capabilities();
const terms = await client.terms();
const yaml = await client.schema();
// Commerce loop
const results = await client.search({ query: "vanilla", limit: 10 });
const quote = await client.quote({ product_id: results.results[0].id, qty: 3 });
const reservation = await client.reserve({ quote_token: quote.quote_token });
// Later...
await client.cancelReservation({ reservation_id: reservation.reservation_id });
// Sessions
const session = await client.identify();
await client.sessionExtend({ session_id: session.session_id });
const me = await client.whoami();
// Every call updates last-seen state — useful for dashboards and back-pressure
console.log(client.lastRateLimit); // { limit, remaining, reset } | null
console.log(client.lastTraceId);Dynamic token refresh
Pass a function for kyaToken when you need per-call token minting (e.g., rotating JWTs or refresh-flow callbacks). The client awaits it on every authenticated request.
import { FacetClient } from "@facet/client";
const client = new FacetClient({
terminalUrl: "https://facet.acme-ingredients.com/v1",
kyaToken: async () => await myIssuer.mintToken(),
});Error handling
Every non-2xx with a valid Facet error envelope becomes a FacetClientError. Other non-2xx responses (HTML 502s, raw strings, non-envelope JSON) become FacetTransportError.
import { FacetClient, FacetClientError } from "@facet/client";
try {
const quote = await client.quote({ product_id: "sku-0002", qty: 9999 });
} catch (e) {
if (e instanceof FacetClientError) {
switch (e.code) {
case "INVENTORY_UNAVAILABLE":
// Use e.suggest to pivot to a search
if (e.suggest?.tool === "search_products") {
await client.search(e.suggest.args as SearchRequest);
}
break;
case "RATE_LIMITED":
// Honor the back-pressure signal
await sleep((e.retryAfterSeconds ?? 1) * 1000);
// ...retry
break;
case "QUOTE_EXPIRED":
// Get a fresh quote
break;
default:
if (e.retryable) {
// exponential backoff
}
}
}
}Idempotency
Pass an idempotencyKey per-call on state-mutating methods (reserve, cancelReservation, future settle / refundRequest). The Terminal dedups by (agent, key) + body hash — a retry with the same key returns the cached response; a retry with the same key but a different body returns 409 IDEMPOTENCY_CONFLICT.
const id = crypto.randomUUID();
try {
const r = await client.reserve({ quote_token }, { idempotencyKey: id });
} catch (e) {
if (e instanceof FacetClientError && e.retryable) {
// Safe to retry with the SAME key — won't double-reserve.
const r = await client.reserve({ quote_token }, { idempotencyKey: id });
}
}Cancellation + timeouts
Default per-call timeout is 30s. Override via timeoutMs, and pass an AbortSignal to compose with external cancellation.
const client = new FacetClient({
terminalUrl: "...",
kyaToken: "...",
timeoutMs: 5_000,
});
const ac = new AbortController();
setTimeout(() => ac.abort(), 2_000);
await client.search({ query: "sugar" }, { signal: ac.signal });Methods
| Method | Route | Auth | Purpose |
| ------------------------ | ----------------------------- | ---- | ----------------------------------------------------- |
| schema() | GET /v1/schema | no | The facet.yaml manifest (YAML string). |
| version() | GET /v1/version | no | Protocol + terminal versions. |
| health() | GET /v1/health | no | Liveness probe. |
| capabilities() | GET /v1/capabilities | no | Which tools + features this tenant exposes. |
| terms() | GET /v1/terms | no | Pricing + rate limits + SLA + data-use advertisement. |
| hello() | POST /v1/hello | yes | Echo agent identity (sanity endpoint). |
| search(req) | POST /v1/search | yes | search_products tool. |
| quote(req) | POST /v1/quote | yes | quote_product tool. |
| reserve(req) | POST /v1/reserve | yes | Verify quote token + hold inventory (TTL 300s). |
| cancelReservation(req) | POST /v1/cancel_reservation | yes | Release a held reservation. |
| identify() | POST /v1/identify | yes | Mint a session (TTL 24h) for the calling agent. |
| sessionExtend(req) | POST /v1/session_extend | yes | Extend a session's expires_at. |
| whoami() | POST /v1/whoami | yes | Return the caller's aid + apd. |
License
Apache 2.0 — same as @facet/protocol. Free to use, free to fork. The hosted Terminal service, schema generator, and admin app remain proprietary; the client contract is an open protocol.
