qati-sdk 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,78 @@
1
+ QATI SDK PROPRIETARY LICENSE
2
+
3
+ Copyright (c) 2024–2026 Qlok Tech. All rights reserved.
4
+
5
+ IMPORTANT — READ CAREFULLY BEFORE USING THIS SOFTWARE.
6
+
7
+ By downloading, installing, copying, or otherwise using the QATI SDK
8
+ ("Software"), you agree to be bound by the terms of this license
9
+ ("License"). If you do not agree to these terms, do not use the Software.
10
+
11
+ 1. GRANT OF LICENSE
12
+
13
+ Subject to the terms and conditions of this License and your compliance
14
+ with the QATI Terms of Service (https://qlokinc.com/terms-of-use), Qlok Tech
15
+ grants you a limited, non-exclusive, non-transferable, non-sublicensable
16
+ license to use the Software solely to interact with the QATI platform
17
+ APIs using a valid API key issued by Qlok Tech.
18
+
19
+ 2. RESTRICTIONS
20
+
21
+ You may NOT, without prior written permission from Qlok Tech:
22
+
23
+ a. Copy, modify, adapt, translate, or create derivative works of the
24
+ Software or any portion thereof;
25
+ b. Distribute, sublicense, sell, resell, transfer, assign, or otherwise
26
+ make the Software available to any third party;
27
+ c. Reverse-engineer, decompile, disassemble, or attempt to derive the
28
+ source code of any compiled component of the Software;
29
+ d. Remove, alter, or obscure any proprietary notices, labels, or marks
30
+ on the Software;
31
+ e. Use the Software to build a competing product or service;
32
+ f. Use the Software without a valid QATI API key or in violation of the
33
+ QATI Terms of Service.
34
+
35
+ 3. API KEY REQUIREMENT
36
+
37
+ Use of the Software requires a valid API key issued by Qlok Tech. API
38
+ keys are subject to the QATI Terms of Service and may be revoked at any
39
+ time for violation of those terms.
40
+
41
+ 4. OWNERSHIP
42
+
43
+ Qlok Tech retains all right, title, and interest in and to the Software,
44
+ including all intellectual property rights. This License does not grant
45
+ you any rights to trademarks, service marks, or trade names of Qlok Tech.
46
+
47
+ 5. DISCLAIMER OF WARRANTIES
48
+
49
+ THE SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS
50
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY,
51
+ FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. QLOK TECH DOES
52
+ NOT WARRANT THAT THE SOFTWARE WILL BE ERROR-FREE OR UNINTERRUPTED.
53
+
54
+ 6. LIMITATION OF LIABILITY
55
+
56
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL
57
+ QLOK TECH BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
58
+ CONSEQUENTIAL DAMAGES (INCLUDING LOSS OF PROFITS, DATA, OR BUSINESS
59
+ INTERRUPTION) ARISING OUT OF OR RELATED TO THIS LICENSE OR THE USE OF
60
+ THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
61
+
62
+ 7. TERMINATION
63
+
64
+ This License is effective until terminated. It will terminate automatically
65
+ if you fail to comply with any term herein. Upon termination, you must
66
+ immediately cease all use of the Software and destroy all copies in your
67
+ possession.
68
+
69
+ 8. GOVERNING LAW
70
+
71
+ This License shall be governed by and construed in accordance with the
72
+ laws of the State of Delaware, United States, without regard to its
73
+ conflict-of-law provisions.
74
+
75
+ 9. CONTACT
76
+
77
+ For licensing inquiries or permissions beyond the scope of this License,
78
+ contact: tim@qlok.net
package/README.md ADDED
@@ -0,0 +1,564 @@
1
+ # QATI TypeScript SDK (`qati-sdk`)
2
+
3
+ Developer Reference · v1
4
+
5
+ ## Overview
6
+
7
+ The Qati SDK is the official TypeScript client for the Qati platform. It gives you a clean, type-safe interface to two backend services — the Query API and the Ingestion API — without having to craft raw HTTP requests yourself.
8
+ Everything starts from a `Session` object, which holds your credentials and connection settings. From that session you open a **client** and call methods grouped by feature area.
9
+ The main feature groups on the client are:
10
+
11
+ - `client.tenant` — verify that your API key is accepted.
12
+ - `client.trustState` — read risk and trust scores for users or other entities.
13
+ - `client.advisory` — list persisted advisories for the tenant (with filters and pagination).
14
+ - `client.explain` — fetch a composite explain / attribution payload for a single entity (by entity key).
15
+ - `client.events` — send telemetry events to the ingestion pipeline.
16
+
17
+ No network traffic occurs until you call a method. Opening a client only prepares HTTP connections and resolves configuration.
18
+
19
+ **Positioning:** QATI v1 is an **append-only, non-blocking observability and threat-intelligence layer**. It does **not** perform automated enforcement, blocking, or policy execution — your application owns those decisions.
20
+
21
+ ## Requirements
22
+
23
+ - **Node.js** 24 or newer (`engines` in [`package.json`](package.json)).
24
+ - **TypeScript** 5.x or newer recommended (matches this package’s dev toolchain).
25
+ - A tenant API key. Set `QATI_TENANT_API_KEY`, pass `tenantApiKey` into `new Session({ ... })`, or build a resolved config with `parseQatiConfig` / `resolveQatiConfig`.
26
+ - **Query API base URL** — mandatory; there is **no** built-in default. Set `QATI_QUERY_API_BASE_URL` or pass `queryApiBaseUrl` in the session config (for example `http://localhost:8001` when developing locally).
27
+ - **Ingestion API base URL** — mandatory; there is **no** built-in default. Set `QATI_INGESTION_API_BASE_URL` or pass `ingestionApiBaseUrl` in the session config (for example `http://localhost:8000` when developing locally).
28
+
29
+ ## Installation
30
+
31
+ Install from npm (distribution name `qati-sdk`):
32
+
33
+ ```bash
34
+ npm install qati-sdk
35
+ ```
36
+
37
+ Or with Yarn:
38
+
39
+ ```bash
40
+ yarn add qati-sdk
41
+ ```
42
+
43
+ Or with pnpm:
44
+
45
+ ```bash
46
+ pnpm add qati-sdk
47
+ ```
48
+
49
+ The package ships **ESM** and **CJS** via `package.json` `exports`. Types live in `./dist/index.d.ts`.
50
+
51
+ ## Client
52
+
53
+ Open a client with `session.createClient()`, `await` the methods you need, and always **`await client.close()`** in a `finally` block (or on shutdown) so buffered ingestion flushes and timers stop.
54
+
55
+ ```ts
56
+ const session = new Session();
57
+ const client = session.createClient();
58
+ try {
59
+ await client.trustState.getTrustState(...)
60
+ } finally {
61
+ await client.close();
62
+ }
63
+ ```
64
+
65
+ ## Step 1 Verify your credentials
66
+
67
+ Before doing anything else, confirm that your API key is accepted. This prevents hard-to-debug errors later.
68
+
69
+ ```ts
70
+ import { Session } from 'qati-sdk';
71
+
72
+ const session = new Session();
73
+ const client = session.createClient();
74
+ try {
75
+ const result = await client.tenant.verifyCredentials();
76
+ console.log(result.is_valid, result.tenant_id);
77
+ } finally {
78
+ await client.close();
79
+ }
80
+ ```
81
+
82
+ Tip: If `is_valid` is `false`, check that `QATI_TENANT_API_KEY` is set correctly before continuing.
83
+
84
+ ## Step 2 Read trust state
85
+
86
+ Trust state tells you the risk tier and closure score for an entity — typically a user — as computed by the Qati platform. Entities are identified by a **lowercase** entity type string and an opaque id (see `EntityTypeLowerCase` in the SDK: `user`, `device`, `account`, `model`, `session`, `service`).
87
+
88
+ ### Single entity
89
+
90
+ ```ts
91
+ import { Session } from 'qati-sdk';
92
+
93
+ const session = new Session();
94
+ const client = session.createClient();
95
+ try {
96
+ const data = await client.trustState.getTrustState('user', 'user-123', {
97
+ topContributorsLimit: 5,
98
+ });
99
+ console.log(data.risk_tier, data.current_closure_score);
100
+ } finally {
101
+ await client.close();
102
+ }
103
+ ```
104
+
105
+ The response is the requested trust-state data.
106
+
107
+ ### Multiple entities in one round trip
108
+
109
+ Pass an array of ids to `getTrustStates`. If the array is **empty**, the SDK returns `[]` immediately without hitting the network.
110
+
111
+ ```ts
112
+ const batch = await client.trustState.getTrustStates(
113
+ 'user',
114
+ ['user-1', 'user-2'],
115
+ { topContributorsLimit: 3 },
116
+ );
117
+ for (const row of batch) {
118
+ console.log(row.entity_key, row.risk_tier);
119
+ }
120
+ ```
121
+
122
+ ## Query API: advisories and explain
123
+
124
+ ### List advisories
125
+
126
+ Use `client.advisory.listAdvisories()`. `GET /v1/advisories` returns a paginated list of advisories.
127
+
128
+ In TypeScript, **`advisoryType`** and **`severity`** are **required** positional arguments (they map to `advisory_type` and `severity` query params). Pass a `Date` or an ISO-8601 string for `since` / `until`; the SDK serializes dates to ISO-8601 for the query string.
129
+
130
+ ```ts
131
+ import type { AdvisorySeverity, AdvisoryType } from 'qati-sdk';
132
+ import { Session } from 'qati-sdk';
133
+
134
+ const session = new Session();
135
+ const client = session.createClient();
136
+ try {
137
+ const page = await client.advisory.listAdvisories(
138
+ 'USER:user-123',
139
+ 'ANOMALY_CLUSTER' as AdvisoryType,
140
+ 'HIGH' as AdvisorySeverity,
141
+ { limit: 20, offset: 0 },
142
+ );
143
+ console.log(page.total_count, page.advisories.length);
144
+ } finally {
145
+ await client.close();
146
+ }
147
+ ```
148
+
149
+ ### Composite explain for one entity
150
+
151
+ `GET /v1/explain/{entity_key}` returns an `ExplainResponse`. The SDK URL-encodes `entity_key` for the path segment (for example scoped keys that contain `:`). Optional `at_timestamp` accepts a `Date` or ISO-8601 string for historical snapshots.
152
+
153
+ ```ts
154
+ import { Session } from 'qati-sdk';
155
+
156
+ const session = new Session();
157
+ const client = session.createClient();
158
+ try {
159
+ const out = await client.explain.get(
160
+ 'USER:user-123',
161
+ new Date(Date.UTC(2026, 3, 1, 12, 0, 0)),
162
+ );
163
+ console.log(out.closure_score, out.risk_tier);
164
+ } finally {
165
+ await client.close();
166
+ }
167
+ ```
168
+
169
+ ## Step 3 Configure the session
170
+
171
+ For most teams, environment variables are the simplest approach — set them once and the SDK picks them up automatically (via `dotenv` loading `.env` from the working directory when using `new Session()` / `resolveQatiConfig()`). If you need finer control (custom URLs, timeouts, or multiple tenants), pass a config object into `Session`.
172
+
173
+ ### Environment variables
174
+
175
+ Configuration precedence (highest first): explicit fields on `new Session({ ... })` → `QATI_*` environment variables (merged from `process.env` into camelCase) → Zod defaults **only for optional fields** (timeouts, retries, batch sizing). **`QATI_QUERY_API_BASE_URL` and `QATI_INGESTION_API_BASE_URL` have no defaults** — you must supply them via env or constructor config.
176
+
177
+ | Variable | Required | Default | Purpose |
178
+ | --------------------------------------- | -------- | ------- | ------------------------------------------ |
179
+ | `QATI_TENANT_API_KEY` | Yes | — | Your tenant API key |
180
+ | `QATI_QUERY_API_BASE_URL` | Yes | — | Query API base URL (no default) |
181
+ | `QATI_INGESTION_API_BASE_URL` | Yes | — | Ingestion API base URL (no default) |
182
+ | `QATI_TIMEOUT` | No | 30 | Seconds to wait per HTTP request |
183
+ | `QATI_MAX_RETRIES` | No | 3 | Retry attempts on transient failure (1–10) |
184
+ | `QATI_RETRY_BACKOFF_INITIAL_SECONDS` | No | 1 | First backoff delay in seconds |
185
+ | `QATI_RETRY_BACKOFF_MAX_SECONDS` | No | 30 | Maximum backoff delay cap |
186
+ | `QATI_RETRY_JITTER_FRACTION` | No | 0.1 | Random jitter to spread out retries |
187
+ | `QATI_INGESTION_BATCH_SIZE` | No | 100 | Max events buffered before auto-flush |
188
+ | `QATI_INGESTION_FLUSH_INTERVAL_SECONDS` | No | 5 | Max wait before flushing a partial batch |
189
+
190
+ ### Configuration in code
191
+
192
+ ```ts
193
+ import { Session } from 'qati-sdk';
194
+
195
+ const session = new Session({
196
+ tenantApiKey: 'qati-...',
197
+ queryApiBaseUrl: 'https://query.example.com',
198
+ ingestionApiBaseUrl: 'https://ingest.example.com',
199
+ timeout: 15,
200
+ ingestionBatchSize: 50,
201
+ });
202
+ ```
203
+
204
+ Advanced: inject custom `axios` instances per logical API with `session.createClient({ query_api: customAxios, ingestion_api: customAxios })` if your platform requires special TLS settings or proxy configuration.
205
+
206
+ ## Step 4 Build an event payload
207
+
208
+ Sending an event is a deliberate two-step process: first build a valid payload, then hand it to the client. Separating construction from delivery means you can validate your data before any network call is made, and catch mistakes early.
209
+
210
+ The `create*` functions exported from `qati-sdk` accept a typed envelope and return a `RawEventRequest`. If your payload is malformed, **Zod** throws while building — no partial HTTP requests to untangle.
211
+
212
+ Builders set `payload.signal_version` to `'v1'` and `payload.signal_type` for you. The ingestion API assigns the persisted raw-event id (**UUID v7**); responses include `event_id`. You do **not** send a top-level `id` on ingest or set `signal_version` manually on the wire object.
213
+
214
+ Most events share the same outer envelope fields. In strict TypeScript codebases, prefer **inlined literals** per builder so `signal_payload` inference stays narrow; here we use a widened helper plus `as` for readability:
215
+
216
+ ```ts
217
+ const TENANT_ID = '550e8400-e29b-41d4-a716-446655440000';
218
+
219
+ const v1Event = (signalPayload: object, extra: object = {}) => {
220
+ return {
221
+ tenant_id: TENANT_ID,
222
+ timestamp: new Date().toISOString(),
223
+ provenance: {
224
+ mode: 'DETERMINISTIC' as const,
225
+ source_id: 'my-service',
226
+ epoch_counter: 0,
227
+ health_summary: {},
228
+ },
229
+ principal: { user_id: 'user-123' },
230
+ signal_payload: signalPayload,
231
+ ...extra,
232
+ };
233
+ };
234
+ ```
235
+
236
+ ### Available signal types
237
+
238
+ #### Transaction
239
+
240
+ ```ts
241
+ import { createTransactionEvent, type TransactionSignalEvent } from 'qati-sdk';
242
+
243
+ const raw = createTransactionEvent(
244
+ v1Event({ amount: 99.99 }) as TransactionSignalEvent,
245
+ );
246
+ ```
247
+
248
+ #### Auth
249
+
250
+ ```ts
251
+ import { createAuthEvent, type AuthSignalEvent } from 'qati-sdk';
252
+
253
+ const raw = createAuthEvent(
254
+ v1Event({
255
+ result: 'success',
256
+ auth_method: 'password',
257
+ mfa_used: true,
258
+ }) as AuthSignalEvent,
259
+ );
260
+ ```
261
+
262
+ #### Model output
263
+
264
+ ```ts
265
+ import { createModelOutputEvent, type ModelOutputSignalEvent } from 'qati-sdk';
266
+
267
+ const raw = createModelOutputEvent(
268
+ v1Event({
269
+ citation_rate: 0.95,
270
+ eval_window_n: 1000,
271
+ }) as ModelOutputSignalEvent,
272
+ );
273
+ ```
274
+
275
+ #### System telemetry
276
+
277
+ ```ts
278
+ import {
279
+ createSystemTelemetryEvent,
280
+ type SystemTelemetrySignalEvent,
281
+ } from 'qati-sdk';
282
+
283
+ const raw = createSystemTelemetryEvent(
284
+ v1Event({
285
+ metric_name: 'p99_latency_ms',
286
+ value: 120.0,
287
+ baseline: 80.0,
288
+ }) as SystemTelemetrySignalEvent,
289
+ );
290
+ ```
291
+
292
+ #### Anomaly flag
293
+
294
+ `severity` must be between 0 and 1.
295
+
296
+ ```ts
297
+ import { createAnomalyFlagEvent, type AnomalyFlagSignalEvent } from 'qati-sdk';
298
+
299
+ const raw = createAnomalyFlagEvent(
300
+ v1Event({ severity: 0.85 }) as AnomalyFlagSignalEvent,
301
+ );
302
+ ```
303
+
304
+ #### Behavior
305
+
306
+ `deviation_score` must be between 0 and 1.
307
+
308
+ ```ts
309
+ import { createBehaviorEvent, type BehaviorSignalEvent } from 'qati-sdk';
310
+
311
+ const raw = createBehaviorEvent(
312
+ v1Event({ deviation_score: 0.4 }) as BehaviorSignalEvent,
313
+ );
314
+ ```
315
+
316
+ #### Network
317
+
318
+ ```ts
319
+ import { createNetworkEvent, type NetworkSignalEvent } from 'qati-sdk';
320
+
321
+ const raw = createNetworkEvent(
322
+ v1Event({
323
+ ip: '198.51.100.10',
324
+ reputation_score: 0.2,
325
+ threat_score: 0.7,
326
+ }) as NetworkSignalEvent,
327
+ );
328
+ ```
329
+
330
+ Note: At this point `raw` is an in-memory object. Nothing has been sent over the network yet. Proceed to Step 5 to deliver it.
331
+
332
+ ### Canonical wire shape (`RawEventRequest` / `BaseEvent`)
333
+
334
+ HTTP ingestion bodies use **`RawEventRequest`**: `{ tenant_id, payload }` (no client-supplied top-level `id`). The typed **`BaseEvent`** lives inside `payload` (`signal_version`, `signal_type`, `signal_payload`, `principal`, optional `timestamp`, `confidence_hint`, `provenance`, `integrity`). After acceptance, the API returns **`event_id`** in the response body.
335
+
336
+ The product spec’s `source: { system, component, region? }` field is **not** modeled as a first-class Zod field in this SDK; use **`provenance`** and **`principal`** for origin context.
337
+
338
+ ## Step 5 Send events
339
+
340
+ `enqueue` adds one event to an in-memory queue inside your process. It does **not** trigger an immediate HTTP call. The SDK groups queued events and posts them in batches to `POST /v1/events:batch`, making the per-event cost very low.
341
+
342
+ Call `flush` when you need delivery to happen right now — for example, at the end of a script or at a processing checkpoint.
343
+
344
+ ### One event, forced delivery
345
+
346
+ The most explicit pattern: build → enqueue → flush.
347
+
348
+ ```ts
349
+ import { Session, createTransactionEvent } from 'qati-sdk';
350
+
351
+ const TENANT_ID = '550e8400-e29b-41d4-a716-446655440000';
352
+
353
+ const main = async () => {
354
+ const session = new Session();
355
+ const client = session.createClient();
356
+ try {
357
+ const raw = createTransactionEvent({
358
+ tenant_id: TENANT_ID,
359
+ signal_version: 'v1',
360
+ signal_type: 'TRANSACTION',
361
+ timestamp: new Date().toISOString(),
362
+ provenance: {
363
+ mode: 'DETERMINISTIC',
364
+ source_id: 'example',
365
+ epoch_counter: 0,
366
+ health_summary: {},
367
+ },
368
+ principal: { user_id: 'user-123' },
369
+ signal_payload: {
370
+ amount: 42.0,
371
+ bulk_export: false,
372
+ contains_phi: false,
373
+ safety_critical: false,
374
+ },
375
+ });
376
+ await client.events.enqueue(raw);
377
+ await client.events.flush();
378
+ } finally {
379
+ await client.close();
380
+ }
381
+ };
382
+
383
+ void main();
384
+ ```
385
+
386
+ ### Multiple enqueues, single flush
387
+
388
+ Enqueue as many events as you like, then flush once to send them all in a single batch.
389
+
390
+ ```ts
391
+ import {
392
+ Session,
393
+ createTransactionEvent,
394
+ type TransactionSignalEvent,
395
+ } from 'qati-sdk';
396
+
397
+ const TENANT_ID = '550e8400-e29b-41d4-a716-446655440000';
398
+
399
+ const v1Event = (signalPayload: object, extra: object = {}) => {
400
+ return {
401
+ tenant_id: TENANT_ID,
402
+ timestamp: new Date().toISOString(),
403
+ provenance: {
404
+ mode: 'DETERMINISTIC' as const,
405
+ source_id: 'batch-example',
406
+ epoch_counter: 0,
407
+ health_summary: {},
408
+ },
409
+ principal: { user_id: 'user-123' },
410
+ signal_payload: signalPayload,
411
+ ...extra,
412
+ };
413
+ };
414
+
415
+ const main = async () => {
416
+ const session = new Session();
417
+ const client = session.createClient();
418
+ try {
419
+ await client.events.enqueue(
420
+ createTransactionEvent(
421
+ v1Event({ amount: 10.0 }) as TransactionSignalEvent,
422
+ ),
423
+ );
424
+ await client.events.enqueue(
425
+ createTransactionEvent(
426
+ v1Event({ amount: 20.0 }) as TransactionSignalEvent,
427
+ ),
428
+ );
429
+ await client.events.flush();
430
+ } finally {
431
+ await client.close();
432
+ }
433
+ };
434
+
435
+ void main();
436
+ ```
437
+
438
+ ### Automatic batching (no manual flush)
439
+
440
+ If you never call `flush`, the SDK still sends data when either condition is met:
441
+
442
+ - The queue reaches `ingestionBatchSize` events (default **100**), or
443
+ - `ingestionFlushIntervalSeconds` have elapsed since the first buffered event (default **5**).
444
+
445
+ High-throughput traffic fills batches quickly; quieter traffic drains on the timer. Adjust both thresholds with `QATI_INGESTION_BATCH_SIZE` and `QATI_INGESTION_FLUSH_INTERVAL_SECONDS`.
446
+
447
+ Important: The queue lives only in memory. If the process exits or crashes before a batch is sent, those events are lost. For high-stakes pipelines, call `flush()` at processing checkpoints rather than relying solely on the automatic timer.
448
+
449
+ ### Dead-letter hook (TypeScript)
450
+
451
+ Buffered ingestion uses `retry: true` on the batch HTTP call. After retries are exhausted, you can register **`client.events.onIngestionFailure((payload, error) => { ... })`** once to log, metrics, or persist failed batches (do not throw from the callback).
452
+
453
+ ## Step 6 Handle errors
454
+
455
+ All SDK errors inherit from `QatiSDKError`. HTTP failures come back as `QatiAPIError` or a narrower subclass.
456
+
457
+ | Exception | When it fires |
458
+ | -------------------- | ------------------------------------------------------------------ |
459
+ | `QatiAuthError` | 401 or 403 — key missing, expired, or lacking permission. |
460
+ | `QatiNotFoundError` | 404 — the requested resource does not exist. |
461
+ | `QatiRateLimitError` | 429 — too many requests; the built-in retry policy may recover it. |
462
+ | `QatiServerError` | 5xx — server-side fault; the built-in retry policy may recover it. |
463
+ | `QatiAPIError` | Any other HTTP error response. |
464
+ | `QatiConfigError` | Session / config is incomplete or invalid. |
465
+
466
+ On `QatiAPIError` you can inspect `e.detail.status_code`, `e.detail.message`, and `e.detail.request_id` (useful when filing a support ticket).
467
+
468
+ ### Example error handler
469
+
470
+ ```ts
471
+ import {
472
+ Session,
473
+ QatiAPIError,
474
+ QatiAuthError,
475
+ QatiNotFoundError,
476
+ QatiRateLimitError,
477
+ } from 'qati-sdk';
478
+
479
+ type QatiClient = ReturnType<Session['createClient']>;
480
+
481
+ const safeCall = async (client: QatiClient) => {
482
+ try {
483
+ return await client.trustState.getTrustState('user', 'id');
484
+ } catch (e) {
485
+ if (e instanceof QatiAuthError) throw e;
486
+ if (e instanceof QatiNotFoundError) return null;
487
+ if (e instanceof QatiRateLimitError) throw e;
488
+ if (e instanceof QatiAPIError) {
489
+ const log = `${e.detail.status_code} ${e.detail.message} request_id=${e.detail.request_id}`;
490
+ throw new Error(log, { cause: e });
491
+ }
492
+ throw e;
493
+ }
494
+ };
495
+ ```
496
+
497
+ ### Validation errors when building events
498
+
499
+ If you pass an invalid payload to a `create*` function, **Zod** throws before any HTTP call is made. Inspect `error.issues` for a structured description of what is wrong.
500
+
501
+ ```ts
502
+ import { ZodError } from 'zod';
503
+ import { createAnomalyFlagEvent, type AnomalyFlagSignalEvent } from 'qati-sdk';
504
+
505
+ const v1Event = (signalPayload: object, extra: object = {}) => {
506
+ return {
507
+ tenant_id: '550e8400-e29b-41d4-a716-446655440000',
508
+ timestamp: new Date().toISOString(),
509
+ provenance: {
510
+ mode: 'DETERMINISTIC' as const,
511
+ source_id: 'validation-example',
512
+ epoch_counter: 0,
513
+ health_summary: {},
514
+ },
515
+ principal: { user_id: 'user-123' },
516
+ signal_payload: signalPayload,
517
+ ...extra,
518
+ };
519
+ };
520
+
521
+ try {
522
+ createAnomalyFlagEvent(v1Event({ severity: 2.0 }) as AnomalyFlagSignalEvent);
523
+ } catch (e) {
524
+ if (e instanceof ZodError) console.log(e.issues);
525
+ }
526
+ ```
527
+
528
+ ### Retries
529
+
530
+ Transient failures are retried automatically for **`POST /v1/events:batch`** according to `QATI_MAX_RETRIES` and the backoff settings in your session config. Retries cover **429**, **5xx**, and transport errors without a response, using exponential backoff with jitter (see `computeBackoffMs` in the SDK source). Once retries are exhausted you still get a `QatiAPIError`. For other endpoints, the SDK typically performs a single attempt unless you use the low-level HTTP layer with `retry: true`.
531
+
532
+ ## Further reading
533
+
534
+ - HTTP paths, headers, and error shapes: [docs/API.md](../../docs/API.md) (repo root).
535
+ - Live OpenAPI UI (when running the Query API locally): `/v1/docs`.
536
+ - TypeScript source: [`sdks/typescript/src/`](src/).
537
+ - Python SDK (sync/async patterns and feature parity notes): [`sdks/python/README.md`](../python/README.md).
538
+
539
+ ---
540
+
541
+ ## Appendix: API surface (compact)
542
+
543
+ | Symbol | Role |
544
+ | --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
545
+ | `Session` | `new Session(config?)`, `session.config`, `session.createClient(httpClients?)`. |
546
+ | `Client` | Namespaces: `tenant`, `trustState`, `advisory`, `explain`, `events`; `await client.close()`. |
547
+ | `client.events` | `enqueue`, `flush`, `shutdown`, `pendingCount`, `onIngestionFailure`. |
548
+ | `create*Event` | `createTransactionEvent`, `createAuthEvent`, `createModelOutputEvent`, `createSystemTelemetryEvent`, `createAnomalyFlagEvent`, `createBehaviorEvent`, `createNetworkEvent` — all exported from `qati-sdk`. |
549
+ | Config | `resolveQatiConfig`, `parseQatiConfig`, `baseUrlFor`, types `QatiConfigInput` / `QatiConfigOutput`. |
550
+ | Errors | `QatiSDKError`, `QatiAPIError`, `QatiAuthError`, `QatiNotFoundError`, `QatiRateLimitError`, `QatiServerError`, `QatiConfigError`. |
551
+
552
+ `HttpClient.request(...)` exists for advanced use; prefer resource methods for application code.
553
+
554
+ ## Build (maintainers)
555
+
556
+ From `sdks/typescript/`:
557
+
558
+ ```bash
559
+ npm ci
560
+ npm test
561
+ npm run build
562
+ ```
563
+
564
+ Outputs land in `dist/` (ESM + CJS + declarations).