qati-sdk 1.0.4 → 1.0.6

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/README.md CHANGED
@@ -13,6 +13,8 @@ The main feature groups on the client are:
13
13
  - `client.advisory` — list persisted advisories for the tenant (with filters and pagination).
14
14
  - `client.explain` — fetch a composite explain / attribution payload for a single entity (by entity key).
15
15
  - `client.events` — send telemetry events to the ingestion pipeline.
16
+ - `client.webhooks` — manage webhook endpoints for a tenant (register, list, update, delete, trigger test delivery).
17
+ - `client.quotas` — read and configure numeric caps (entities, monthly events, rate) for a tenant.
16
18
 
17
19
  No network traffic occurs until you call a method. Opening a client only prepares HTTP connections and resolves configuration.
18
20
 
@@ -166,6 +168,138 @@ try {
166
168
  }
167
169
  ```
168
170
 
171
+ ## Webhooks
172
+
173
+ `client.webhooks` manages webhook endpoints for a specific tenant. All methods require `tenantId` as their first argument — resolve it once with `client.tenant.verifyCredentials()` and reuse the value.
174
+
175
+ Supported event types: `trust_state.tier_escalated`, `advisory.created`.
176
+
177
+ ### Register a webhook
178
+
179
+ ```ts
180
+ import { Session } from 'qati-sdk';
181
+
182
+ const session = new Session();
183
+ const client = session.createClient();
184
+ try {
185
+ const creds = await client.tenant.verifyCredentials();
186
+ const webhook = await client.webhooks.create(creds.tenant_id, {
187
+ url: 'https://your-service.example.com/qati-events',
188
+ subscribed_event_types: ['trust_state.tier_escalated', 'advisory.created'],
189
+ enabled: true,
190
+ });
191
+ // webhook.secret is returned exactly once — store it for HMAC verification.
192
+ console.log(webhook.id, webhook.secret);
193
+ } finally {
194
+ await client.close();
195
+ }
196
+ ```
197
+
198
+ The `secret` field in the response is shown **only on creation**. Store it securely; you cannot retrieve it again. To rotate the secret, use `patch` with `rotate_secret: true`.
199
+
200
+ ### List webhooks
201
+
202
+ ```ts
203
+ const hooks = await client.webhooks.list(tenantId);
204
+ for (const h of hooks) {
205
+ console.log(h.id, h.url, h.enabled, h.consecutive_failures);
206
+ }
207
+ ```
208
+
209
+ ### Update a webhook
210
+
211
+ Any field is optional. Pass `rotate_secret: true` to get a new signing secret (returned in `new_secret`).
212
+
213
+ ```ts
214
+ const updated = await client.webhooks.patch(tenantId, webhookId, {
215
+ enabled: false,
216
+ subscribed_event_types: ['advisory.created'],
217
+ });
218
+ ```
219
+
220
+ ### Trigger a test delivery
221
+
222
+ Enqueues a synthetic event to the webhook. Useful for verifying your receiver end-to-end.
223
+
224
+ ```ts
225
+ const { delivery_id } = await client.webhooks.test(tenantId, webhookId);
226
+ console.log('test delivery queued:', delivery_id);
227
+ ```
228
+
229
+ ### Delete a webhook
230
+
231
+ ```ts
232
+ await client.webhooks.delete(tenantId, webhookId);
233
+ ```
234
+
235
+ ### Verifying signatures
236
+
237
+ The platform signs every delivery with HMAC-SHA256. The signature is in the `X-QATI-Signature` header as `v1=<hex>`. The signed message is `"<unix_timestamp>.<raw_utf8_body>"` where the timestamp comes from `X-QATI-Timestamp`.
238
+
239
+ ```ts
240
+ import crypto from 'crypto';
241
+
242
+ function verifySignature(
243
+ rawBody: string,
244
+ timestamp: string,
245
+ signature: string,
246
+ secret: string,
247
+ ): boolean {
248
+ const expected =
249
+ 'v1=' +
250
+ crypto
251
+ .createHmac('sha256', secret)
252
+ .update(`${timestamp}.${rawBody}`, 'utf8')
253
+ .digest('hex');
254
+ if (signature.length !== expected.length) return false;
255
+ return crypto.timingSafeEqual(
256
+ Buffer.from(signature, 'utf8'),
257
+ Buffer.from(expected, 'utf8'),
258
+ );
259
+ }
260
+ ```
261
+
262
+ Use `express.text({ type: 'application/json' })` (not `express.json()`) to capture the raw body string before parsing, so the HMAC is computed over the exact bytes the platform signed.
263
+
264
+ ---
265
+
266
+ ## Quotas
267
+
268
+ `client.quotas` reads and writes the numeric caps that govern how much a tenant can send and store.
269
+
270
+ ### Get current quotas
271
+
272
+ Returns the active limits; throws `QatiNotFoundError` (404) if no quota has been configured for the tenant yet.
273
+
274
+ ```ts
275
+ import { Session } from 'qati-sdk';
276
+
277
+ const session = new Session();
278
+ const client = session.createClient();
279
+ try {
280
+ const creds = await client.tenant.verifyCredentials();
281
+ const quota = await client.quotas.get(creds.tenant_id);
282
+ console.log(quota.max_tracked_entities, quota.max_monthly_events, quota.max_events_per_second);
283
+ } finally {
284
+ await client.close();
285
+ }
286
+ ```
287
+
288
+ ### Upsert quotas
289
+
290
+ Creates or fully replaces the quota configuration. All three fields are required.
291
+
292
+ ```ts
293
+ const quota = await client.quotas.upsert(tenantId, {
294
+ max_tracked_entities: 50000,
295
+ max_monthly_events: 5_000_000,
296
+ max_events_per_second: 1000,
297
+ });
298
+ console.log(quota.id, quota.tenant_id);
299
+ ```
300
+
301
+ ---
302
+
169
303
  ## Step 3 Configure the session
170
304
 
171
305
  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`.
@@ -540,13 +674,15 @@ Transient failures are retried automatically for **`POST /v1/events:batch`** acc
540
674
 
541
675
  ## Appendix: API surface (compact)
542
676
 
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`. |
677
+ | Symbol | Role |
678
+ | ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
679
+ | `Session` | `new Session(config?)`, `session.config`, `session.createClient(httpClients?)`. |
680
+ | `Client` | Namespaces: `tenant`, `trustState`, `advisory`, `explain`, `events`, `webhooks`, `quotas`; `await client.close()`. |
681
+ | `client.events` | `enqueue`, `flush`, `shutdown`, `pendingCount`, `onIngestionFailure`. |
682
+ | `client.webhooks` | `list(tenantId)`, `create(tenantId, body)`, `patch(tenantId, webhookId, body)`, `delete(tenantId, webhookId)`, `test(tenantId, webhookId)`. |
683
+ | `client.quotas` | `get(tenantId)`, `upsert(tenantId, body)`. |
684
+ | `create*Event` | `createTransactionEvent`, `createAuthEvent`, `createModelOutputEvent`, `createSystemTelemetryEvent`, `createAnomalyFlagEvent`, `createBehaviorEvent`, `createNetworkEvent` — all exported from `qati-sdk`. |
685
+ | Config | `resolveQatiConfig`, `parseQatiConfig`, `baseUrlFor`, types `QatiConfigInput` / `QatiConfigOutput`. |
686
+ | Errors | `QatiSDKError`, `QatiAPIError`, `QatiAuthError`, `QatiNotFoundError`, `QatiRateLimitError`, `QatiServerError`, `QatiConfigError`. |
551
687
 
552
688
  `HttpClient.request(...)` exists for advanced use; prefer resource methods for application code.
package/dist/index.cjs CHANGED
@@ -78,19 +78,6 @@ var QatiConfigError = class extends QatiSDKError {
78
78
  };
79
79
 
80
80
  // src/config.ts
81
- var EV_VARIABLES_NAMES = [
82
- "QATI_TENANT_API_KEY",
83
- "QATI_QUERY_API_BASE_URL",
84
- "QATI_INGESTION_API_BASE_URL",
85
- "QATI_TIMEOUT",
86
- "QATI_MAX_RETRIES",
87
- "QATI_INGESTION_BATCH_SIZE",
88
- "QATI_INGESTION_FLUSH_INTERVAL_SECONDS",
89
- "QATI_RETRY_BACKOFF_INITIAL_SECONDS",
90
- "QATI_RETRY_BACKOFF_MAX_SECONDS",
91
- "QATI_RETRY_JITTER_FRACTION",
92
- "QATI_MAX_RETRIES"
93
- ];
94
81
  var API_REGISTRY = {
95
82
  query_api: "queryApiBaseUrl",
96
83
  ingestion_api: "ingestionApiBaseUrl"
@@ -115,14 +102,22 @@ var stripUndefined = (obj) => {
115
102
  };
116
103
  var readEnvOverrides = () => {
117
104
  const e = process.env;
118
- const sanitizedEnvs = EV_VARIABLES_NAMES.reduce(
119
- (acc, ev) => {
120
- acc[ev] = e[ev];
121
- return acc;
122
- },
123
- {}
124
- );
125
- return sanitizedEnvs;
105
+ const num = (key) => {
106
+ const v = e[key];
107
+ return v !== void 0 ? Number(v) : void 0;
108
+ };
109
+ return {
110
+ tenantApiKey: e["QATI_TENANT_API_KEY"],
111
+ queryApiBaseUrl: e["QATI_QUERY_API_BASE_URL"],
112
+ ingestionApiBaseUrl: e["QATI_INGESTION_API_BASE_URL"],
113
+ timeout: num("QATI_TIMEOUT"),
114
+ maxRetries: num("QATI_MAX_RETRIES"),
115
+ ingestionBatchSize: num("QATI_INGESTION_BATCH_SIZE"),
116
+ ingestionFlushIntervalSeconds: num("QATI_INGESTION_FLUSH_INTERVAL_SECONDS"),
117
+ retryBackoffInitialSeconds: num("QATI_RETRY_BACKOFF_INITIAL_SECONDS"),
118
+ retryBackoffMaxSeconds: num("QATI_RETRY_BACKOFF_MAX_SECONDS"),
119
+ retryJitterFraction: num("QATI_RETRY_JITTER_FRACTION")
120
+ };
126
121
  };
127
122
  var resolveQatiConfig = (input) => {
128
123
  dotenv.config({ path: ".env" });
@@ -607,6 +602,29 @@ var ExplainResource = class {
607
602
  }
608
603
  };
609
604
 
605
+ // src/resources/quota-resource.ts
606
+ var QuotaResource = class {
607
+ constructor(_http) {
608
+ this._http = _http;
609
+ }
610
+ _http;
611
+ apiName = "query_api";
612
+ async get(tenantId) {
613
+ return this._http.request(
614
+ "GET",
615
+ `/v1/tenants/${encodeURIComponent(tenantId)}/quotas`,
616
+ { api_name: this.apiName }
617
+ );
618
+ }
619
+ async upsert(tenantId, body) {
620
+ return this._http.request(
621
+ "PUT",
622
+ `/v1/tenants/${encodeURIComponent(tenantId)}/quotas`,
623
+ { api_name: this.apiName, data: body }
624
+ );
625
+ }
626
+ };
627
+
610
628
  // src/resources/tenant-resource.ts
611
629
  var TenantResource = class {
612
630
  constructor(_http) {
@@ -680,6 +698,50 @@ var TrustStateResource = class {
680
698
  }
681
699
  };
682
700
 
701
+ // src/resources/webhook-resource.ts
702
+ var WebhookResource = class {
703
+ constructor(_http) {
704
+ this._http = _http;
705
+ }
706
+ _http;
707
+ apiName = "query_api";
708
+ async list(tenantId) {
709
+ return this._http.request(
710
+ "GET",
711
+ `/v1/tenants/${encodeURIComponent(tenantId)}/webhooks`,
712
+ { api_name: this.apiName }
713
+ );
714
+ }
715
+ async create(tenantId, body) {
716
+ return this._http.request(
717
+ "POST",
718
+ `/v1/tenants/${encodeURIComponent(tenantId)}/webhooks`,
719
+ { api_name: this.apiName, data: body }
720
+ );
721
+ }
722
+ async patch(tenantId, webhookId, body) {
723
+ return this._http.request(
724
+ "PATCH",
725
+ `/v1/tenants/${encodeURIComponent(tenantId)}/webhooks/${encodeURIComponent(webhookId)}`,
726
+ { api_name: this.apiName, data: body }
727
+ );
728
+ }
729
+ async delete(tenantId, webhookId) {
730
+ const instance = this._http.getClient(this.apiName);
731
+ await instance.request({
732
+ method: "DELETE",
733
+ url: `/v1/tenants/${encodeURIComponent(tenantId)}/webhooks/${encodeURIComponent(webhookId)}`
734
+ });
735
+ }
736
+ async test(tenantId, webhookId) {
737
+ return this._http.request(
738
+ "POST",
739
+ `/v1/tenants/${encodeURIComponent(tenantId)}/webhooks/${encodeURIComponent(webhookId)}/test`,
740
+ { api_name: this.apiName }
741
+ );
742
+ }
743
+ };
744
+
683
745
  // src/client/client.ts
684
746
  var Client = class extends HttpClient {
685
747
  _tenant;
@@ -687,6 +749,8 @@ var Client = class extends HttpClient {
687
749
  _advisory;
688
750
  _explain;
689
751
  _events;
752
+ _webhooks;
753
+ _quotas;
690
754
  /**
691
755
  * @param config - Resolved SDK configuration (typically `session.config`).
692
756
  * @param axiosInstances - Optional per-API `axios` instances; see {@link Session#createClient}.
@@ -699,6 +763,8 @@ var Client = class extends HttpClient {
699
763
  this._advisory = new AdvisoryResource(this);
700
764
  this._explain = new ExplainResource(this);
701
765
  this._events = new EventResource(this);
766
+ this._webhooks = new WebhookResource(this);
767
+ this._quotas = new QuotaResource(this);
702
768
  }
703
769
  /**
704
770
  * Tenant API surface (credentials verification).
@@ -740,6 +806,24 @@ var Client = class extends HttpClient {
740
806
  get events() {
741
807
  return this._events;
742
808
  }
809
+ /**
810
+ * Webhook endpoint management for a tenant (create, list, patch, delete, test delivery).
811
+ *
812
+ * All methods require `tenantId` explicitly; resolve it once via `client.tenant.verifyCredentials()`.
813
+ *
814
+ * @returns {@link WebhookResource} for the Query API.
815
+ */
816
+ get webhooks() {
817
+ return this._webhooks;
818
+ }
819
+ /**
820
+ * Tenant quota configuration (get and upsert numeric caps).
821
+ *
822
+ * @returns {@link QuotaResource} for the Query API.
823
+ */
824
+ get quotas() {
825
+ return this._quotas;
826
+ }
743
827
  /**
744
828
  * Flushes the event ingestion buffer (best-effort), then shuts down the batch scheduler.
745
829
  * Call this on process shutdown so queued events are not lost.