Skip to content

TypeScript SDK (@telha/sdk)

One client surface, two wires: RestTransport (fetch + x-api-key) for external consumers, GrpcTransport (signed tenant tokens over @grpc/grpc-js) for in-cluster app-layer traffic. Source: sdk/typescript/.

Install

Not published to npm yet. Today the package is consumed from the workspace (the app layer references it as a workspace dependency). From your own project, use a path dependency against a checkout:

cd telha-v2/sdk/typescript && npm install && npm run build
# then in your project:
npm install /path/to/telha-v2/sdk/typescript

ESM only ("type": "module"), TypeScript types shipped from dist/. Runtime dependencies are @grpc/grpc-js and @grpc/proto-loader (only exercised when you construct a GrpcTransport).

Client construction

REST (external consumers)

import { TelhaClient, RestTransport } from "@telha/sdk";

const client = new TelhaClient(
  new RestTransport({
    url: "http://127.0.0.1:7625",
    apiKey: process.env.TELHA_API_KEY!,
    // retries: 2,      // idempotent requests only, on 5xx/transport errors
    // backoffMs: 100,  // jittered exponential base
  }),
);

gRPC (in-cluster app layer)

GrpcTransport speaks telha.v1.AppLayerService and mints a signed tenant token per call (metadata x-telha-token) from the shared grpc.token_key:

import { TelhaClient, GrpcTransport } from "@telha/sdk";

const client = new TelhaClient(
  new GrpcTransport({
    address: "127.0.0.1:7626",        // or "unix:/path/to.sock" (Unix only)
    tokenKeyHex: process.env.TELHA_GRPC_TOKEN_KEY!, // core's grpc.token_key, 64 hex
    tenantId: "1111...",
    organizationId: "2222...",
    // audience: "app" (default), deadlineMs: 30000, retries: 2, backoffMs: 100
  }),
);

REST-only surfaces on GrpcTransport (PR-028 reality). The gRPC app-layer surface maps only these REST shapes onto RPCs:

REST shape RPC
POST /v1/query Query
POST /v1/snapshot Snapshot
POST /v1/compare Compare
POST /v1/records CreateRecords
PUT /v1/records/:id UpdateRecord
POST /v1/relationships BatchWrite

Everything else - records.get, records.history, records.delete, schema, relationship delete - throws UnimplementedError with code GRPC_UNMAPPED (status 501) telling you to use RestTransport. There is no silent fallback.

API surface

// query / snapshot / compare / schema
const { records, related, nextCursor, stats } = await client.query({
  find: "RISK",
  where: { severity: { $gte: 7 } },   // operator objects, never bare scalars
  expand: [{ type: "OWNED_BY", direction: "out", depth: 1 }],
  limit: 50,
});

const asOf = await client.snapshot({ find: "RISK", atValidTime: "2026-06-01T00:00:00Z" });

const delta = await client.compare({
  baseline: { validTime: "2026-06-01T00:00:00Z" },
  comparison: { validTime: "2026-07-01T00:00:00Z" },
  find: "RISK",
});
// delta.added / delta.removed (reason: "tombstone" | "validityLapse") /
// delta.modified (per-property old -> new) / delta.stats / delta.nextCursor

const { schemas } = await client.schema({ label: "RISK", at: 1750000000000000 });

// records
const [created] = await client.records.create({
  labels: ["RISK"],
  properties: { title: "Supplier delay", severity: 7 },
  relationships: [{ type: "OWNED_BY", node: personId }],
});
const current = await client.records.get(created.id);
const page = await client.records.history(created.id, { limit: 50 });
const updated = await client.records.update(created.id, {
  labels: ["RISK"],
  properties: { title: "Supplier delay", severity: 8 },
}); // full-snapshot update: appends a new version, history preserved
const receipt = await client.records.delete(created.id); // tombstone cascade

// relationships
const { id: edgeId } = await client.relationships.create({
  from: created.id, to: personId, type: "OWNED_BY",
});
await client.relationships.delete(edgeId);

query<T>() and snapshot<T>() take a type parameter to type the returned records. Ingest (POST /v1/ingest) has no dedicated wrapper in the TS client today; call it over REST directly (or use the Python SDK, which wraps it).

Query grammar reference

The living reference for the query DSL is the fixture corpus at core-engine/tests/fixtures/query_corpus.json: every accepted shape and every rejection with its expected JSON pointer, pinned by core tests. If a query shape is not in the corpus, do not rely on it.

Errors

Non-2xx responses map the envelope {code, message, request_id} onto a typed hierarchy (sdk/typescript/src/errors.ts), all extending TelhaError { code, status, requestId }:

Class When
QueryInvalidError 400 QUERY_INVALID (message carries the JSON pointer)
ValidationError other 4xx (BAD_CURSOR, INVALID_INTERVAL, EMPTY_BATCH, ...)
AuthError 401 UNAUTHORIZED
NotFoundError 404
ConflictError 409 (TOMBSTONED, TYPE_COLLISION, ALREADY_EXISTS)
UnimplementedError 501
ServerError 5xx INTERNAL and transport failures (status 0, code TRANSPORT)

gRPC status codes map onto the same taxonomy (fromGrpcError): UNAUTHENTICATEDAuthError, INVALID_ARGUMENTQueryInvalidError/ValidationError, NOT_FOUND, ALREADY_EXISTS/ ABORTEDConflictError, UNIMPLEMENTED, DEADLINE_EXCEEDED/ UNAVAILABLEServerError.

Retry semantics

  • Only requests marked idempotent retry: reads, query, snapshot, compare. Writes are never retried by the SDK.
  • REST: retried on 5xx and transport errors only; 4xx is thrown immediately. Default retries: 2 extra attempts with jittered exponential backoff (backoffMs * 2^(attempt-1) * (0.5 + rand)).
  • gRPC: retried on UNAVAILABLE and DEADLINE_EXCEEDED only, same backoff shape, per-call deadline 30 s by default.

Token helpers

The SDK exports the signed-token primitives (byte-compatible with core's grpc/token.rs; TTL 300 s):

import { signTenantToken, signServiceToken,
         AUDIENCE_APP, AUDIENCE_MCP, AUDIENCE_CLARIFY } from "@telha/sdk";

const mcpToken = signTenantToken(tokenKeyHex, {
  tenantId, organizationId, audience: AUDIENCE_MCP,
});
const pollToken = signServiceToken(tokenKeyHex, {
  workerId: "clarify-bot", audience: AUDIENCE_CLARIFY,
});

These are what the clarify-bot and MCP clients use; operators can mint equivalent tokens manually with telha api-key mcp-token|clarify-token.

See also