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 +78 -0
- package/README.md +564 -0
- package/dist/index.cjs +1021 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2147 -0
- package/dist/index.d.ts +2147 -0
- package/dist/index.js +996 -0
- package/dist/index.js.map +1 -0
- package/package.json +54 -0
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).
|