@ciphyrshq/sdk 2.6.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/README.md +194 -0
- package/package.json +30 -0
- package/src/client.js +973 -0
- package/src/errors.js +52 -0
- package/src/eval-runner.js +127 -0
- package/src/index.js +13 -0
- package/src/secret-detector.js +49 -0
- package/src/tracer.js +220 -0
- package/types.d.ts +382 -0
package/src/client.js
ADDED
|
@@ -0,0 +1,973 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CiphyrsError,
|
|
3
|
+
CiphyrsAuthError,
|
|
4
|
+
CiphyrsPermissionError,
|
|
5
|
+
CiphyrsNotFoundError,
|
|
6
|
+
CiphyrsRateLimitError,
|
|
7
|
+
CiphyrsTimeoutError,
|
|
8
|
+
CiphyrsJobTimeoutError,
|
|
9
|
+
} from './errors.js';
|
|
10
|
+
|
|
11
|
+
const DEFAULT_BASE_URL = 'https://www.ciphyrs.com';
|
|
12
|
+
const DASH_BASE_URL = 'https://www.ciphyrs.com';
|
|
13
|
+
|
|
14
|
+
const RETRYABLE_STATUSES = new Set([429, 500, 502, 503, 504]);
|
|
15
|
+
const MAX_RETRIES = 3;
|
|
16
|
+
const BASE_DELAY_MS = 500;
|
|
17
|
+
|
|
18
|
+
// ── Internal HTTP helper with auto-retry ────────────────────────────────────────
|
|
19
|
+
async function request(url, { method = 'GET', headers = {}, body, timeout = 10_000, maxRetries = MAX_RETRIES } = {}) {
|
|
20
|
+
let lastErr;
|
|
21
|
+
|
|
22
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
23
|
+
let res;
|
|
24
|
+
try {
|
|
25
|
+
res = await fetch(url, {
|
|
26
|
+
method,
|
|
27
|
+
signal: AbortSignal.timeout(timeout),
|
|
28
|
+
headers: { 'Content-Type': 'application/json', ...headers },
|
|
29
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
30
|
+
});
|
|
31
|
+
} catch (err) {
|
|
32
|
+
if (err.name === 'TimeoutError' || err.name === 'AbortError') {
|
|
33
|
+
lastErr = new CiphyrsTimeoutError();
|
|
34
|
+
if (attempt < maxRetries) { await sleep(backoff(attempt)); continue; }
|
|
35
|
+
throw lastErr;
|
|
36
|
+
}
|
|
37
|
+
lastErr = new CiphyrsError(err.message);
|
|
38
|
+
if (attempt < maxRetries) { await sleep(backoff(attempt)); continue; }
|
|
39
|
+
throw lastErr;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const data = await res.json().catch(() => ({ error: res.statusText }));
|
|
43
|
+
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const msg = data?.error || `Request failed (${res.status})`;
|
|
46
|
+
|
|
47
|
+
// Retry on transient errors
|
|
48
|
+
if (RETRYABLE_STATUSES.has(res.status) && attempt < maxRetries) {
|
|
49
|
+
const retryAfter = res.headers.get('retry-after');
|
|
50
|
+
const delay = retryAfter ? parseInt(retryAfter) * 1000 : backoff(attempt);
|
|
51
|
+
await sleep(delay);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (res.status === 401) throw new CiphyrsAuthError(msg);
|
|
56
|
+
if (res.status === 403) throw new CiphyrsPermissionError(msg);
|
|
57
|
+
if (res.status === 404) throw new CiphyrsNotFoundError(msg);
|
|
58
|
+
if (res.status === 429) throw new CiphyrsRateLimitError(msg, { retryAfter: res.headers.get('retry-after') });
|
|
59
|
+
throw new CiphyrsError(msg, { status: res.status });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw lastErr || new CiphyrsError('Request failed after retries');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function backoff(attempt) {
|
|
69
|
+
return BASE_DELAY_MS * Math.pow(2, attempt) + Math.random() * 200;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sleep(ms) {
|
|
73
|
+
return new Promise(r => setTimeout(r, ms));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
77
|
+
// ScanResource — mask / restore / async jobs
|
|
78
|
+
// Used by: developers integrating Ciphyrs into their LLM pipelines
|
|
79
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
80
|
+
class ScanResource {
|
|
81
|
+
#headers; #base; #timeout;
|
|
82
|
+
|
|
83
|
+
constructor(headers, base, timeout) {
|
|
84
|
+
this.#headers = headers;
|
|
85
|
+
this.#base = base;
|
|
86
|
+
this.#timeout = timeout;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Mask PII in text synchronously.
|
|
91
|
+
* Returns the masked text + a sessionId to pass to restore() later.
|
|
92
|
+
*/
|
|
93
|
+
async mask(text, {
|
|
94
|
+
sessionId, entities, source = 'PROMPT',
|
|
95
|
+
surface = 'sdk', llmProvider,
|
|
96
|
+
} = {}) {
|
|
97
|
+
const d = await request(`${this.#base}/v1/scan/mask`, {
|
|
98
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
99
|
+
body: { text, session_id: sessionId, entities, source, surface, llm_provider: llmProvider },
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
maskedText: d.masked_text,
|
|
103
|
+
sessionId: d.session_id,
|
|
104
|
+
entitiesFound: d.entities_found,
|
|
105
|
+
entitySummary: d.entity_summary,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Submit a mask job asynchronously — returns a jobId immediately (202).
|
|
111
|
+
* Poll getJob() or use waitForJob() to retrieve the result.
|
|
112
|
+
*/
|
|
113
|
+
async maskAsync(text, {
|
|
114
|
+
sessionId, entities, source = 'PROMPT',
|
|
115
|
+
surface = 'sdk', llmProvider, webhookUrl,
|
|
116
|
+
} = {}) {
|
|
117
|
+
const d = await request(`${this.#base}/v1/scan/mask/async`, {
|
|
118
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
119
|
+
body: {
|
|
120
|
+
text, session_id: sessionId, entities, source, surface,
|
|
121
|
+
llm_provider: llmProvider, webhook_url: webhookUrl,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
return { jobId: d.job_id, sessionId: d.session_id, status: d.status };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Poll for an async job result. */
|
|
128
|
+
async getJob(jobId) {
|
|
129
|
+
const d = await request(`${this.#base}/v1/scan/jobs/${encodeURIComponent(jobId)}`, {
|
|
130
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
jobId: d.job_id,
|
|
134
|
+
status: d.status, // 'queued' | 'processing' | 'done' | 'failed'
|
|
135
|
+
sessionId: d.session_id,
|
|
136
|
+
maskedText: d.masked_text,
|
|
137
|
+
entitiesFound: d.entities_found,
|
|
138
|
+
entitySummary: d.entity_summary,
|
|
139
|
+
latencyMs: d.latency_ms,
|
|
140
|
+
error: d.error,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Wait for an async job to finish, polling automatically.
|
|
146
|
+
* Throws CiphyrsJobTimeoutError if it doesn't complete within timeoutMs.
|
|
147
|
+
*/
|
|
148
|
+
async waitForJob(jobId, { pollIntervalMs = 500, timeoutMs = 60_000 } = {}) {
|
|
149
|
+
const deadline = Date.now() + timeoutMs;
|
|
150
|
+
while (Date.now() < deadline) {
|
|
151
|
+
const job = await this.getJob(jobId);
|
|
152
|
+
if (job.status === 'done' || job.status === 'failed') return job;
|
|
153
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
154
|
+
}
|
|
155
|
+
throw new CiphyrsJobTimeoutError(jobId, timeoutMs);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Restore original PII values from a masked text using the session vault.
|
|
160
|
+
* @param {string} maskedText — The masked text containing placeholder tokens.
|
|
161
|
+
* @param {string} sessionId — The session ID from the original mask call.
|
|
162
|
+
* @param {object} [opts]
|
|
163
|
+
* @param {boolean} [opts.purge=false] — Delete tokens from vault after restore.
|
|
164
|
+
*/
|
|
165
|
+
async restore(maskedText, sessionId, { purge = false } = {}) {
|
|
166
|
+
const d = await request(`${this.#base}/v1/scan/restore`, {
|
|
167
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
168
|
+
body: { text: maskedText, session_id: sessionId, purge },
|
|
169
|
+
});
|
|
170
|
+
return { restoredText: d.restored_text, tokensRestored: d.tokens_restored, purged: d.purged };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* High-level wrapper for the mask → LLM call → restore round-trip.
|
|
175
|
+
*
|
|
176
|
+
* The #1 bug we see in customer integrations is "I called mask() and
|
|
177
|
+
* sent the masked text to my LLM, but my user is seeing [PERSON_1] in
|
|
178
|
+
* the response" — they forgot the restore() step. protect() makes this
|
|
179
|
+
* impossible to get wrong.
|
|
180
|
+
*
|
|
181
|
+
* @param {string} userInput — Raw text from your end-user.
|
|
182
|
+
* @param {(masked: string, ctx: { sessionId: string, entitiesFound: any[] }) => Promise<string>} llmCall
|
|
183
|
+
* Async function that calls your LLM with the masked text and
|
|
184
|
+
* returns the LLM's response (a string).
|
|
185
|
+
* @param {object} [opts]
|
|
186
|
+
* @param {string} [opts.sessionId] — Reuse a vault session across calls.
|
|
187
|
+
* @param {string[]}[opts.entities] — Entity types to mask (defaults to all).
|
|
188
|
+
* @param {string} [opts.source='PROMPT']
|
|
189
|
+
* @param {string} [opts.surface='sdk']
|
|
190
|
+
* @param {string} [opts.llmProvider]
|
|
191
|
+
* @param {boolean} [opts.purge=true] — Flush vault entries after restore.
|
|
192
|
+
* Defaults to true (data minimisation);
|
|
193
|
+
* set false to keep the session alive
|
|
194
|
+
* for follow-up turns in a conversation.
|
|
195
|
+
* @returns {Promise<{
|
|
196
|
+
* output: string, // Unmasked LLM response (return THIS to the user)
|
|
197
|
+
* maskedInput: string, // What the LLM actually saw (audit)
|
|
198
|
+
* maskedOutput: string, // What the LLM returned, before unmask (audit)
|
|
199
|
+
* sessionId: string, // Vault session id
|
|
200
|
+
* entitiesFound: any[], // PII entities masked in the input
|
|
201
|
+
* tokensRestored: number // How many placeholders were unmasked in the output
|
|
202
|
+
* }>}
|
|
203
|
+
*
|
|
204
|
+
* @example
|
|
205
|
+
* const result = await client.scan.protect(
|
|
206
|
+
* userMessage,
|
|
207
|
+
* async (masked) => {
|
|
208
|
+
* const r = await openai.chat.completions.create({
|
|
209
|
+
* model: 'gpt-4o',
|
|
210
|
+
* messages: [{ role: 'user', content: masked }],
|
|
211
|
+
* });
|
|
212
|
+
* return r.choices[0].message.content;
|
|
213
|
+
* }
|
|
214
|
+
* );
|
|
215
|
+
* res.send(result.output); // → "Hello John, ..." not "Hello [PERSON_1], ..."
|
|
216
|
+
*/
|
|
217
|
+
async protect(userInput, llmCall, opts = {}) {
|
|
218
|
+
if (typeof userInput !== 'string') {
|
|
219
|
+
throw new TypeError('protect: userInput must be a string');
|
|
220
|
+
}
|
|
221
|
+
if (typeof llmCall !== 'function') {
|
|
222
|
+
throw new TypeError('protect: llmCall must be an async function (masked, ctx) => Promise<string>');
|
|
223
|
+
}
|
|
224
|
+
const { purge = true, ...maskOpts } = opts;
|
|
225
|
+
|
|
226
|
+
// 1. Mask
|
|
227
|
+
const m = await this.mask(userInput, maskOpts);
|
|
228
|
+
|
|
229
|
+
// 2. Call the customer's LLM with the masked text
|
|
230
|
+
let llmResponse;
|
|
231
|
+
try {
|
|
232
|
+
llmResponse = await llmCall(m.maskedText, {
|
|
233
|
+
sessionId: m.sessionId,
|
|
234
|
+
entitiesFound: m.entitiesFound,
|
|
235
|
+
});
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// Customer's LLM failed — flush the vault so we don't leak the session
|
|
238
|
+
this.restore('', m.sessionId, { purge: true }).catch(() => {});
|
|
239
|
+
throw err;
|
|
240
|
+
}
|
|
241
|
+
if (typeof llmResponse !== 'string') {
|
|
242
|
+
throw new TypeError(
|
|
243
|
+
'protect: llmCall must return a string (the LLM response). ' +
|
|
244
|
+
'Got ' + typeof llmResponse + '. Extract the text field from your provider response object first.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 3. Restore — and purge by default
|
|
249
|
+
const r = await this.restore(llmResponse, m.sessionId, { purge });
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
output: r.restoredText,
|
|
253
|
+
maskedInput: m.maskedText,
|
|
254
|
+
maskedOutput: llmResponse,
|
|
255
|
+
sessionId: m.sessionId,
|
|
256
|
+
entitiesFound: m.entitiesFound,
|
|
257
|
+
tokensRestored: r.tokensRestored,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
263
|
+
// GuardResource — V58 inline allow/block decision for agent runtime
|
|
264
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
265
|
+
//
|
|
266
|
+
// The "Datadog APM auto-instrument" equivalent for security: customers wrap
|
|
267
|
+
// their LLM calls with check() and it returns allow / block / review in
|
|
268
|
+
// <50ms. The whole V58 pitch lives here.
|
|
269
|
+
class GuardResource {
|
|
270
|
+
#headers; #base; #timeout;
|
|
271
|
+
constructor(headers, base, timeout) {
|
|
272
|
+
this.#headers = headers; this.#base = base; this.#timeout = timeout;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Synchronous attack check. Returns { decision, detections, ... }.
|
|
277
|
+
*
|
|
278
|
+
* @param {object} params
|
|
279
|
+
* @param {string} [params.input] — Text from your end-user.
|
|
280
|
+
* @param {string} [params.output] — Optional: agent's output (output-side checks).
|
|
281
|
+
* @param {string} [params.agentName]
|
|
282
|
+
* @param {string} [params.traceId]
|
|
283
|
+
* @param {string} [params.spanId]
|
|
284
|
+
* @param {string} [params.sessionId]
|
|
285
|
+
* @param {string} [params.userId] — Hashed before storage; never raw.
|
|
286
|
+
* @param {string} [params.operationName]
|
|
287
|
+
* @param {string} [params.policyOverride] — block_attacks | block_critical | review | observe
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* const guard = await client.guard.check({
|
|
291
|
+
* input: userMessage,
|
|
292
|
+
* agentName: 'support-bot',
|
|
293
|
+
* userId: req.user.id,
|
|
294
|
+
* });
|
|
295
|
+
* if (guard.decision === 'block') return res.status(400).send({ error: guard.reason });
|
|
296
|
+
* const llmReply = await openai.chat.completions.create({ ... });
|
|
297
|
+
*/
|
|
298
|
+
async check({
|
|
299
|
+
input, output, agentName, traceId, spanId, sessionId,
|
|
300
|
+
userId, operationName, policyOverride,
|
|
301
|
+
} = {}) {
|
|
302
|
+
return request(`${this.#base}/v1/guard/check`, {
|
|
303
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
304
|
+
body: {
|
|
305
|
+
input, output,
|
|
306
|
+
agent_name: agentName,
|
|
307
|
+
trace_id: traceId,
|
|
308
|
+
span_id: spanId,
|
|
309
|
+
session_id: sessionId,
|
|
310
|
+
user_id: userId,
|
|
311
|
+
operation_name: operationName,
|
|
312
|
+
policy_override: policyOverride,
|
|
313
|
+
},
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* One-shot wrapper: check input, if allowed call your LLM, then check
|
|
319
|
+
* output, then return. Matches the protect() pattern but for blocking.
|
|
320
|
+
*
|
|
321
|
+
* @example
|
|
322
|
+
* const result = await client.guard.wrap(userMessage, async (input) => {
|
|
323
|
+
* return await openai.chat.completions.create({...}).choices[0].message.content;
|
|
324
|
+
* });
|
|
325
|
+
* if (result.blocked) return res.status(400).send({ error: result.reason });
|
|
326
|
+
* res.send(result.output);
|
|
327
|
+
*/
|
|
328
|
+
async wrap(userInput, llmCall, opts = {}) {
|
|
329
|
+
if (typeof llmCall !== 'function') {
|
|
330
|
+
throw new TypeError('guard.wrap: llmCall must be a function');
|
|
331
|
+
}
|
|
332
|
+
// 1. Pre-check input
|
|
333
|
+
const inGuard = await this.check({ ...opts, input: userInput });
|
|
334
|
+
if (inGuard.decision === 'block') {
|
|
335
|
+
return { blocked: true, reason: inGuard.reason, decision: inGuard.decision,
|
|
336
|
+
detections: inGuard.detections, decision_id: inGuard.decision_id };
|
|
337
|
+
}
|
|
338
|
+
// 2. Call the customer's LLM
|
|
339
|
+
const llmResponse = await llmCall(userInput);
|
|
340
|
+
if (typeof llmResponse !== 'string') {
|
|
341
|
+
throw new TypeError('guard.wrap: llmCall must return a string');
|
|
342
|
+
}
|
|
343
|
+
// 3. Post-check output
|
|
344
|
+
const outGuard = await this.check({ ...opts, input: userInput, output: llmResponse });
|
|
345
|
+
if (outGuard.decision === 'block') {
|
|
346
|
+
return { blocked: true, reason: outGuard.reason, decision: outGuard.decision,
|
|
347
|
+
detections: outGuard.detections, decision_id: outGuard.decision_id,
|
|
348
|
+
output: null }; // never leak the blocked output
|
|
349
|
+
}
|
|
350
|
+
return { blocked: false, output: llmResponse, decision: outGuard.decision,
|
|
351
|
+
detections: outGuard.detections, decision_id: outGuard.decision_id };
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async getPolicy() {
|
|
355
|
+
return request(`${this.#base}/v1/guard/policy`, { headers: this.#headers, timeout: this.#timeout });
|
|
356
|
+
}
|
|
357
|
+
async setPolicy(policyMode) {
|
|
358
|
+
return request(`${this.#base}/v1/guard/policy`, {
|
|
359
|
+
method: 'PUT', headers: this.#headers, timeout: this.#timeout,
|
|
360
|
+
body: { policy_mode: policyMode },
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
async decisions({ limit, decision } = {}) {
|
|
364
|
+
const q = new URLSearchParams();
|
|
365
|
+
if (limit) q.set('limit', String(limit));
|
|
366
|
+
if (decision) q.set('decision', decision);
|
|
367
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
368
|
+
return request(`${this.#base}/v1/guard/decisions${qs}`, { headers: this.#headers, timeout: this.#timeout });
|
|
369
|
+
}
|
|
370
|
+
async stats({ days = 7 } = {}) {
|
|
371
|
+
return request(`${this.#base}/v1/guard/stats?days=${days}`, { headers: this.#headers, timeout: this.#timeout });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
376
|
+
// SecurityResource — V55-V59 detections, rules, canaries, intel, marketplace
|
|
377
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
378
|
+
class SecurityResource {
|
|
379
|
+
#headers; #base; #timeout;
|
|
380
|
+
constructor(headers, base, timeout) {
|
|
381
|
+
this.#headers = headers; this.#base = base; this.#timeout = timeout;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ── Detections (V55) ──────────────────────────────────────────────
|
|
385
|
+
async listDetections(params = {}) {
|
|
386
|
+
const q = new URLSearchParams();
|
|
387
|
+
for (const [k, v] of Object.entries(params)) if (v != null) q.set(k, String(v));
|
|
388
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
389
|
+
return request(`${this.#base}/v1/security/detections${qs}`, { headers: this.#headers, timeout: this.#timeout });
|
|
390
|
+
}
|
|
391
|
+
async detectionSummary({ days = 30 } = {}) {
|
|
392
|
+
return request(`${this.#base}/v1/security/detections/summary?days=${days}`, { headers: this.#headers, timeout: this.#timeout });
|
|
393
|
+
}
|
|
394
|
+
async detectionTimeseries({ days = 7 } = {}) {
|
|
395
|
+
return request(`${this.#base}/v1/security/detections/timeseries?days=${days}`, { headers: this.#headers, timeout: this.#timeout });
|
|
396
|
+
}
|
|
397
|
+
async detectionsByTrace(traceId) {
|
|
398
|
+
return request(`${this.#base}/v1/security/detections/by-trace/${encodeURIComponent(traceId)}`,
|
|
399
|
+
{ headers: this.#headers, timeout: this.#timeout });
|
|
400
|
+
}
|
|
401
|
+
async detectionContext(detectionId) {
|
|
402
|
+
return request(`${this.#base}/v1/security/detections/${encodeURIComponent(detectionId)}/context`,
|
|
403
|
+
{ headers: this.#headers, timeout: this.#timeout });
|
|
404
|
+
}
|
|
405
|
+
async setDetectionStatus(id, status, note) {
|
|
406
|
+
return request(`${this.#base}/v1/security/detections/${encodeURIComponent(id)}/disposition`, {
|
|
407
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
408
|
+
body: { status, note },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// ── Custom rules (V56) ────────────────────────────────────────────
|
|
413
|
+
async listRules() {
|
|
414
|
+
return request(`${this.#base}/v1/security/rules`, { headers: this.#headers, timeout: this.#timeout });
|
|
415
|
+
}
|
|
416
|
+
async createRule(rule) {
|
|
417
|
+
return request(`${this.#base}/v1/security/rules`, {
|
|
418
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout, body: rule,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
async updateRule(id, rule) {
|
|
422
|
+
return request(`${this.#base}/v1/security/rules/${encodeURIComponent(id)}`, {
|
|
423
|
+
method: 'PATCH', headers: this.#headers, timeout: this.#timeout, body: rule,
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
async deleteRule(id) {
|
|
427
|
+
return request(`${this.#base}/v1/security/rules/${encodeURIComponent(id)}`, {
|
|
428
|
+
method: 'DELETE', headers: this.#headers, timeout: this.#timeout,
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// ── Canaries (V59) ─────────────────────────────────────────────────
|
|
433
|
+
async listCanaries() {
|
|
434
|
+
return request(`${this.#base}/v1/security/canaries`, { headers: this.#headers, timeout: this.#timeout });
|
|
435
|
+
}
|
|
436
|
+
async createCanary(canary) {
|
|
437
|
+
return request(`${this.#base}/v1/security/canaries`, {
|
|
438
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout, body: canary,
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async deleteCanary(id) {
|
|
442
|
+
return request(`${this.#base}/v1/security/canaries/${encodeURIComponent(id)}`, {
|
|
443
|
+
method: 'DELETE', headers: this.#headers, timeout: this.#timeout,
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// ── Threat intel (V59) ─────────────────────────────────────────────
|
|
448
|
+
async listIntelFeeds() {
|
|
449
|
+
return request(`${this.#base}/v1/security/threat-intel`, { headers: this.#headers, timeout: this.#timeout });
|
|
450
|
+
}
|
|
451
|
+
async refreshIntelFeed(id) {
|
|
452
|
+
return request(`${this.#base}/v1/security/threat-intel/${encodeURIComponent(id)}/refresh`, {
|
|
453
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ── Marketplace (V59) ──────────────────────────────────────────────
|
|
458
|
+
async marketplaceList(params = {}) {
|
|
459
|
+
const q = new URLSearchParams();
|
|
460
|
+
for (const [k, v] of Object.entries(params)) if (v != null) q.set(k, String(v));
|
|
461
|
+
const qs = q.toString() ? `?${q.toString()}` : '';
|
|
462
|
+
return request(`${this.#base}/v1/security/marketplace${qs}`, { headers: this.#headers, timeout: this.#timeout });
|
|
463
|
+
}
|
|
464
|
+
async marketplaceInstall(id) {
|
|
465
|
+
return request(`${this.#base}/v1/security/marketplace/${encodeURIComponent(id)}/install`, {
|
|
466
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ── Benchmark (V59) ────────────────────────────────────────────────
|
|
471
|
+
async runBenchmark(target = 'classifier') {
|
|
472
|
+
return request(`${this.#base}/v1/security/benchmark/run`, {
|
|
473
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
474
|
+
body: { target },
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async benchmarkRuns() {
|
|
478
|
+
return request(`${this.#base}/v1/security/benchmark/runs`, { headers: this.#headers, timeout: this.#timeout });
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// ── SIEM targets (V58) ─────────────────────────────────────────────
|
|
482
|
+
async listSiemTargets() {
|
|
483
|
+
return request(`${this.#base}/v1/security/siem`, { headers: this.#headers, timeout: this.#timeout });
|
|
484
|
+
}
|
|
485
|
+
async createSiemTarget(target) {
|
|
486
|
+
return request(`${this.#base}/v1/security/siem`, {
|
|
487
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout, body: target,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
async testSiemTarget(id) {
|
|
491
|
+
return request(`${this.#base}/v1/security/siem/${encodeURIComponent(id)}/test`, {
|
|
492
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ── Replay (V57) ───────────────────────────────────────────────────
|
|
497
|
+
async listReplayJobs() {
|
|
498
|
+
return request(`${this.#base}/v1/security/replay`, { headers: this.#headers, timeout: this.#timeout });
|
|
499
|
+
}
|
|
500
|
+
async createReplayJob(body) {
|
|
501
|
+
return request(`${this.#base}/v1/security/replay`, {
|
|
502
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout, body,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
508
|
+
// ReportsResource — V54 PDF + share
|
|
509
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
510
|
+
class ReportsResource {
|
|
511
|
+
#headers; #base; #timeout;
|
|
512
|
+
constructor(headers, base, timeout) {
|
|
513
|
+
this.#headers = headers; this.#base = base; this.#timeout = timeout;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Generate a date-range prod report.
|
|
518
|
+
* @returns {Promise<{ report: { id, title, ... } }>}
|
|
519
|
+
*/
|
|
520
|
+
async generateProd({
|
|
521
|
+
title, rangeStart, rangeEnd, sections, format = 'pdf',
|
|
522
|
+
} = {}) {
|
|
523
|
+
return request(`${this.#base}/v1/reports/prod`, {
|
|
524
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
525
|
+
body: {
|
|
526
|
+
title,
|
|
527
|
+
range_start: rangeStart instanceof Date ? rangeStart.toISOString() : rangeStart,
|
|
528
|
+
range_end: rangeEnd instanceof Date ? rangeEnd.toISOString() : rangeEnd,
|
|
529
|
+
sections, format,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
async listProd() {
|
|
535
|
+
return request(`${this.#base}/v1/reports/prod`, { headers: this.#headers, timeout: this.#timeout });
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Download a prod report PDF as a Blob (browser) or Buffer (Node).
|
|
540
|
+
* @returns {Promise<Blob|Buffer>}
|
|
541
|
+
*/
|
|
542
|
+
async downloadProdPdf(reportId) {
|
|
543
|
+
const url = `${this.#base}/v1/reports/prod/${encodeURIComponent(reportId)}/pdf`;
|
|
544
|
+
const res = await fetch(url, { headers: this.#headers });
|
|
545
|
+
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
|
|
546
|
+
if (typeof Blob !== 'undefined' && res.body) return res.blob();
|
|
547
|
+
return Buffer.from(await res.arrayBuffer());
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Download a test-run PDF report (V54 — the security/perf test results).
|
|
552
|
+
*/
|
|
553
|
+
async downloadTestRunPdf(runId) {
|
|
554
|
+
const url = `${this.#base}/v1/reports/test-runs/${encodeURIComponent(runId)}/pdf`;
|
|
555
|
+
const res = await fetch(url, { headers: this.#headers });
|
|
556
|
+
if (!res.ok) throw new Error(`Download failed: HTTP ${res.status}`);
|
|
557
|
+
if (typeof Blob !== 'undefined' && res.body) return res.blob();
|
|
558
|
+
return Buffer.from(await res.arrayBuffer());
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Mint a public share link (no auth required to download).
|
|
563
|
+
* @returns {Promise<{ share_token, share_expires_at, share_url_path }>}
|
|
564
|
+
*/
|
|
565
|
+
async share(reportId, ttlDays = 7) {
|
|
566
|
+
return request(`${this.#base}/v1/reports/prod/${encodeURIComponent(reportId)}/share`, {
|
|
567
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
568
|
+
body: { ttl_days: ttlDays },
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
async archive(reportId) {
|
|
573
|
+
return request(`${this.#base}/v1/reports/prod/${encodeURIComponent(reportId)}`, {
|
|
574
|
+
method: 'DELETE', headers: this.#headers, timeout: this.#timeout,
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
580
|
+
// AuthResource — registration, login, API key management
|
|
581
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
582
|
+
class AuthResource {
|
|
583
|
+
#headers; #base; #timeout;
|
|
584
|
+
|
|
585
|
+
constructor(headers, base, timeout) {
|
|
586
|
+
this.#headers = headers;
|
|
587
|
+
this.#base = base;
|
|
588
|
+
this.#timeout = timeout;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/** Register a new account. Returns the JWT token and first API key. */
|
|
592
|
+
async register(email, password, companyName) {
|
|
593
|
+
return request(`${this.#base}/v1/auth/register`, {
|
|
594
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
595
|
+
body: { email, password, company_name: companyName },
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/** Log in to an existing account. Returns a JWT token. */
|
|
600
|
+
async login(email, password) {
|
|
601
|
+
return request(`${this.#base}/v1/auth/login`, {
|
|
602
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
603
|
+
body: { email, password },
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/** Create a new API key (requires JWT auth, owner/admin only). */
|
|
608
|
+
async createApiKey({ name, scopes = ['scan'] } = {}) {
|
|
609
|
+
return request(`${this.#base}/v1/auth/api-keys`, {
|
|
610
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
611
|
+
body: { name, scopes },
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** List all API keys for the current tenant. */
|
|
616
|
+
async listApiKeys() {
|
|
617
|
+
return request(`${this.#base}/v1/auth/api-keys`, {
|
|
618
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/** Revoke an API key by ID (owner/admin only). */
|
|
623
|
+
async revokeApiKey(id) {
|
|
624
|
+
return request(`${this.#base}/v1/auth/api-keys/${encodeURIComponent(id)}`, {
|
|
625
|
+
method: 'DELETE', headers: this.#headers, timeout: this.#timeout,
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
631
|
+
// MetricsResource — dashboard analytics (requires JWT auth)
|
|
632
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
633
|
+
class MetricsResource {
|
|
634
|
+
#headers; #dash; #timeout;
|
|
635
|
+
|
|
636
|
+
constructor(headers, dash, timeout) {
|
|
637
|
+
this.#headers = headers;
|
|
638
|
+
this.#dash = dash;
|
|
639
|
+
this.#timeout = timeout;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
#get(path, params) {
|
|
643
|
+
const url = new URL(`${this.#dash}/metrics${path}`);
|
|
644
|
+
if (params) Object.entries(params).forEach(([k, v]) => v != null && url.searchParams.set(k, v));
|
|
645
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/** Overall scan statistics (total events, PII masked, comparisons vs prev period). */
|
|
649
|
+
summary() { return this.#get('/summary'); }
|
|
650
|
+
|
|
651
|
+
/** Daily aggregate timeseries. range: '7d' | '30d' | '90d' */
|
|
652
|
+
timeseries({ range = '30d' } = {}) { return this.#get('/timeseries', { range }); }
|
|
653
|
+
|
|
654
|
+
/** Top 20 entity types detected (last 30 days). */
|
|
655
|
+
byEntity() { return this.#get('/by-entity'); }
|
|
656
|
+
|
|
657
|
+
/** Detection counts grouped by surface (vscode, browser, mcp, api, sdk). */
|
|
658
|
+
bySurface() { return this.#get('/by-surface'); }
|
|
659
|
+
|
|
660
|
+
/** Detection counts grouped by source (PROMPT, PASTE, RAG, TOOL_RESULT, AGENT_MEMORY). */
|
|
661
|
+
bySource() { return this.#get('/by-source'); }
|
|
662
|
+
|
|
663
|
+
/** Activity breakdown per API key / developer. */
|
|
664
|
+
byDeveloper() { return this.#get('/by-developer'); }
|
|
665
|
+
|
|
666
|
+
/** Recent detection events (paginated). */
|
|
667
|
+
recentEvents({ limit = 50, offset = 0 } = {}) {
|
|
668
|
+
return this.#get('/recent-events', { limit, offset });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/** Single event detail by ID. */
|
|
672
|
+
eventDetail(eventId) { return this.#get(`/events/${encodeURIComponent(eventId)}`); }
|
|
673
|
+
|
|
674
|
+
/** Detection counts grouped by API key (last 30 days). */
|
|
675
|
+
byApiKey() { return this.#get('/by-api-key'); }
|
|
676
|
+
|
|
677
|
+
/** Latency percentiles (p50/p95/p99) per day (last 30 days). */
|
|
678
|
+
latencyPercentiles() { return this.#get('/latency-percentiles'); }
|
|
679
|
+
|
|
680
|
+
/** Scan counts by day-of-week and hour — heatmap data (last 30 days). */
|
|
681
|
+
peakHours() { return this.#get('/peak-hours'); }
|
|
682
|
+
|
|
683
|
+
/** Compliance report for a date range — suitable for GDPR / DPDP export. */
|
|
684
|
+
complianceReport({ from, to } = {}) { return this.#get('/compliance-report', { from, to }); }
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
688
|
+
// TenantResource — profile and team management (requires JWT auth)
|
|
689
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
690
|
+
class TenantResource {
|
|
691
|
+
#headers; #dash; #timeout;
|
|
692
|
+
|
|
693
|
+
constructor(headers, dash, timeout) {
|
|
694
|
+
this.#headers = headers;
|
|
695
|
+
this.#dash = dash;
|
|
696
|
+
this.#timeout = timeout;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Get tenant profile (name, plan, seats, status, settings). */
|
|
700
|
+
profile() {
|
|
701
|
+
return request(`${this.#dash}/profile`, {
|
|
702
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** List team members (owner/admin only). */
|
|
707
|
+
members() {
|
|
708
|
+
return request(`${this.#dash}/members`, {
|
|
709
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/** Invite a user to the tenant (owner/admin only). */
|
|
714
|
+
invite(email, role = 'member') {
|
|
715
|
+
return request(`${this.#dash}/members/invite`, {
|
|
716
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
717
|
+
body: { email, role },
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** Update tenant-level settings (custom entities, webhook URL, etc.). */
|
|
722
|
+
updateSettings(settings) {
|
|
723
|
+
return request(`${this.#dash}/settings`, {
|
|
724
|
+
method: 'PATCH', headers: this.#headers, timeout: this.#timeout,
|
|
725
|
+
body: settings,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
731
|
+
// TraceResource — AgentTrace observability & security
|
|
732
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
733
|
+
class TraceResource {
|
|
734
|
+
#headers; #base; #timeout;
|
|
735
|
+
|
|
736
|
+
constructor(headers, base, timeout) {
|
|
737
|
+
this.#headers = headers;
|
|
738
|
+
this.#base = base;
|
|
739
|
+
this.#timeout = timeout;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** Ingest a single trace with its spans. */
|
|
743
|
+
async ingest(project, trace, spans) {
|
|
744
|
+
return request(`${this.#base}/v1/trace/ingest`, {
|
|
745
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
746
|
+
body: { project, trace, spans },
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** Ingest multiple traces in a single request. */
|
|
751
|
+
async ingestBatch(traces) {
|
|
752
|
+
return request(`${this.#base}/v1/trace/ingest/batch`, {
|
|
753
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
754
|
+
body: { traces },
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/** Append spans to an existing trace. */
|
|
759
|
+
async appendSpans(traceId, spans) {
|
|
760
|
+
return request(`${this.#base}/v1/trace/spans`, {
|
|
761
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
762
|
+
body: { trace_id: traceId, spans },
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
/** List traces with optional filters. */
|
|
767
|
+
async list({ status, projectId, hasPii, limit, offset } = {}) {
|
|
768
|
+
const url = new URL(`${this.#base}/v1/trace/`);
|
|
769
|
+
if (status != null) url.searchParams.set('status', status);
|
|
770
|
+
if (projectId != null) url.searchParams.set('project_id', projectId);
|
|
771
|
+
if (hasPii != null) url.searchParams.set('has_pii', hasPii);
|
|
772
|
+
if (limit != null) url.searchParams.set('limit', limit);
|
|
773
|
+
if (offset != null) url.searchParams.set('offset', offset);
|
|
774
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
/** Get a single trace by ID. */
|
|
778
|
+
async get(traceId) {
|
|
779
|
+
return request(`${this.#base}/v1/trace/${encodeURIComponent(traceId)}`, {
|
|
780
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/** Get raw trace data (unprocessed spans/events). */
|
|
785
|
+
async getRaw(traceId) {
|
|
786
|
+
return request(`${this.#base}/v1/trace/${encodeURIComponent(traceId)}/raw`, {
|
|
787
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/** Update trace metadata. */
|
|
792
|
+
async update(traceId, updates) {
|
|
793
|
+
return request(`${this.#base}/v1/trace/${encodeURIComponent(traceId)}`, {
|
|
794
|
+
method: 'PATCH', headers: this.#headers, timeout: this.#timeout,
|
|
795
|
+
body: updates,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** List traced projects. */
|
|
800
|
+
async listProjects({ limit, offset } = {}) {
|
|
801
|
+
const url = new URL(`${this.#base}/v1/trace/projects`);
|
|
802
|
+
if (limit != null) url.searchParams.set('limit', limit);
|
|
803
|
+
if (offset != null) url.searchParams.set('offset', offset);
|
|
804
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
/** Create a new traced project. */
|
|
808
|
+
async createProject(name, opts = {}) {
|
|
809
|
+
return request(`${this.#base}/v1/trace/projects`, {
|
|
810
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
811
|
+
body: { name, ...opts },
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/** Get a traced project by ID. */
|
|
816
|
+
async getProject(id) {
|
|
817
|
+
return request(`${this.#base}/v1/trace/projects/${encodeURIComponent(id)}`, {
|
|
818
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
/** List discovered agents. */
|
|
823
|
+
async listAgents({ projectId, limit, offset } = {}) {
|
|
824
|
+
const url = new URL(`${this.#base}/v1/trace/agents`);
|
|
825
|
+
if (projectId != null) url.searchParams.set('project_id', projectId);
|
|
826
|
+
if (limit != null) url.searchParams.set('limit', limit);
|
|
827
|
+
if (offset != null) url.searchParams.set('offset', offset);
|
|
828
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
/** Get a single agent by ID. */
|
|
832
|
+
async getAgent(id) {
|
|
833
|
+
return request(`${this.#base}/v1/trace/agents/${encodeURIComponent(id)}`, {
|
|
834
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/** Update an agent's role and description. */
|
|
839
|
+
async updateAgentRole(id, role, description) {
|
|
840
|
+
return request(`${this.#base}/v1/trace/agents/${encodeURIComponent(id)}/role`, {
|
|
841
|
+
method: 'PATCH', headers: this.#headers, timeout: this.#timeout,
|
|
842
|
+
body: { role, description },
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/** List security events with optional filters. */
|
|
847
|
+
async listSecurityEvents({ severity, status, projectId, limit, offset } = {}) {
|
|
848
|
+
const url = new URL(`${this.#base}/v1/trace/security/events`);
|
|
849
|
+
if (severity != null) url.searchParams.set('severity', severity);
|
|
850
|
+
if (status != null) url.searchParams.set('status', status);
|
|
851
|
+
if (projectId != null) url.searchParams.set('project_id', projectId);
|
|
852
|
+
if (limit != null) url.searchParams.set('limit', limit);
|
|
853
|
+
if (offset != null) url.searchParams.set('offset', offset);
|
|
854
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/** Get security events summary/aggregation. */
|
|
858
|
+
async getSecuritySummary({ projectId, range } = {}) {
|
|
859
|
+
const url = new URL(`${this.#base}/v1/trace/security/events/summary`);
|
|
860
|
+
if (projectId != null) url.searchParams.set('project_id', projectId);
|
|
861
|
+
if (range != null) url.searchParams.set('range', range);
|
|
862
|
+
return request(url.toString(), { headers: this.#headers, timeout: this.#timeout });
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** Resolve a security event by ID. */
|
|
866
|
+
async resolveSecurityEvent(id, note) {
|
|
867
|
+
return request(`${this.#base}/v1/trace/security/events/${encodeURIComponent(id)}/resolve`, {
|
|
868
|
+
method: 'PATCH', headers: this.#headers, timeout: this.#timeout,
|
|
869
|
+
body: { note },
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/** List security rules. */
|
|
874
|
+
async listRules() {
|
|
875
|
+
return request(`${this.#base}/v1/trace/security/rules`, {
|
|
876
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Create a security rule. */
|
|
881
|
+
async createRule(rule) {
|
|
882
|
+
return request(`${this.#base}/v1/trace/security/rules`, {
|
|
883
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
884
|
+
body: rule,
|
|
885
|
+
});
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
/** List security webhooks. */
|
|
889
|
+
async listWebhooks() {
|
|
890
|
+
return request(`${this.#base}/v1/trace/security/webhooks`, {
|
|
891
|
+
headers: this.#headers, timeout: this.#timeout,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/** Create a security webhook. */
|
|
896
|
+
async createWebhook(webhook) {
|
|
897
|
+
return request(`${this.#base}/v1/trace/security/webhooks`, {
|
|
898
|
+
method: 'POST', headers: this.#headers, timeout: this.#timeout,
|
|
899
|
+
body: webhook,
|
|
900
|
+
});
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
905
|
+
// CiphyrsClient — main entry point
|
|
906
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
907
|
+
export class CiphyrsClient {
|
|
908
|
+
/**
|
|
909
|
+
* @param {object} opts
|
|
910
|
+
* @param {string} opts.apiKey — API key (cyp_live_...) for server-side use
|
|
911
|
+
* @param {string} [opts.token] — JWT token for dashboard/management endpoints
|
|
912
|
+
* @param {string} [opts.baseUrl] — Override gateway URL (VPC / on-prem deployments)
|
|
913
|
+
* @param {string} [opts.dashUrl] — Override dashboard API URL
|
|
914
|
+
* @param {number} [opts.timeout] — Request timeout in ms (default: 10000)
|
|
915
|
+
*/
|
|
916
|
+
constructor({ apiKey, token, baseUrl = DEFAULT_BASE_URL, dashUrl = DASH_BASE_URL, timeout = 10_000 } = {}) {
|
|
917
|
+
if (!apiKey && !token) throw new Error('CiphyrsClient: provide apiKey (server-side) or token (dashboard)');
|
|
918
|
+
|
|
919
|
+
const headers = {
|
|
920
|
+
...(apiKey ? { 'x-api-key': apiKey } : {}),
|
|
921
|
+
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
/** @internal Base URL for API requests (used by tracer / eval-runner) */
|
|
925
|
+
this._baseUrl = baseUrl.replace(/\/$/, '');
|
|
926
|
+
/** @internal Default headers (used by tracer / eval-runner) */
|
|
927
|
+
this._headers = headers;
|
|
928
|
+
/** @internal Default timeout */
|
|
929
|
+
this._timeout = timeout;
|
|
930
|
+
|
|
931
|
+
this.scan = new ScanResource(headers, this._baseUrl, timeout);
|
|
932
|
+
this.auth = new AuthResource(headers, this._baseUrl, timeout);
|
|
933
|
+
this.metrics = new MetricsResource(headers, dashUrl.replace(/\/$/, ''), timeout);
|
|
934
|
+
this.tenant = new TenantResource(headers, dashUrl.replace(/\/$/, ''), timeout);
|
|
935
|
+
this.trace = new TraceResource(headers, this._baseUrl, timeout);
|
|
936
|
+
// V54-V59 — security platform resources
|
|
937
|
+
this.guard = new GuardResource(headers, this._baseUrl, timeout);
|
|
938
|
+
this.security = new SecurityResource(headers, this._baseUrl, timeout);
|
|
939
|
+
this.reports = new ReportsResource(headers, this._baseUrl, timeout);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* @internal — Make an authenticated HTTP request.
|
|
944
|
+
* Used by CiphyrsTracer and EvalRunner for endpoints not covered by resource classes.
|
|
945
|
+
*/
|
|
946
|
+
async _request(url, opts = {}) {
|
|
947
|
+
return request(url, {
|
|
948
|
+
...opts,
|
|
949
|
+
headers: { ...this._headers, ...opts.headers },
|
|
950
|
+
timeout: opts.timeout ?? this._timeout,
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// ── Top-level shortcuts for the most common operations ──────────────────────
|
|
955
|
+
|
|
956
|
+
/** Shortcut for client.scan.mask() */
|
|
957
|
+
mask(text, opts) { return this.scan.mask(text, opts); }
|
|
958
|
+
|
|
959
|
+
/** Shortcut for client.scan.restore() */
|
|
960
|
+
restore(maskedText, sessionId, opts) { return this.scan.restore(maskedText, sessionId, opts); }
|
|
961
|
+
|
|
962
|
+
/** Shortcut for client.scan.protect() — mask → LLM call → restore round-trip */
|
|
963
|
+
protect(userInput, llmCall, opts) { return this.scan.protect(userInput, llmCall, opts); }
|
|
964
|
+
|
|
965
|
+
/** Shortcut for client.scan.maskAsync() */
|
|
966
|
+
maskAsync(text, opts) { return this.scan.maskAsync(text, opts); }
|
|
967
|
+
|
|
968
|
+
/** Shortcut for client.scan.waitForJob() */
|
|
969
|
+
waitForJob(jobId, opts) { return this.scan.waitForJob(jobId, opts); }
|
|
970
|
+
|
|
971
|
+
/** Shortcut for client.auth.createApiKey() */
|
|
972
|
+
createKey(opts) { return this.auth.createApiKey(opts); }
|
|
973
|
+
}
|