@tuteliq/sdk 2.1.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 +21 -0
- package/README.md +1034 -0
- package/dist/cjs/client.js +1283 -0
- package/dist/cjs/client.js.map +1 -0
- package/dist/cjs/constants.js +179 -0
- package/dist/cjs/constants.js.map +1 -0
- package/dist/cjs/errors.js +129 -0
- package/dist/cjs/errors.js.map +1 -0
- package/dist/cjs/index.js +37 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/types/account.js +6 -0
- package/dist/cjs/types/account.js.map +1 -0
- package/dist/cjs/types/analysis.js +6 -0
- package/dist/cjs/types/analysis.js.map +1 -0
- package/dist/cjs/types/guidance.js +3 -0
- package/dist/cjs/types/guidance.js.map +1 -0
- package/dist/cjs/types/index.js +28 -0
- package/dist/cjs/types/index.js.map +1 -0
- package/dist/cjs/types/media.js +6 -0
- package/dist/cjs/types/media.js.map +1 -0
- package/dist/cjs/types/policy.js +6 -0
- package/dist/cjs/types/policy.js.map +1 -0
- package/dist/cjs/types/pricing.js +6 -0
- package/dist/cjs/types/pricing.js.map +1 -0
- package/dist/cjs/types/reports.js +3 -0
- package/dist/cjs/types/reports.js.map +1 -0
- package/dist/cjs/types/safety.js +7 -0
- package/dist/cjs/types/safety.js.map +1 -0
- package/dist/cjs/types/voice-stream.js +6 -0
- package/dist/cjs/types/voice-stream.js.map +1 -0
- package/dist/cjs/types/webhooks.js +6 -0
- package/dist/cjs/types/webhooks.js.map +1 -0
- package/dist/cjs/utils/retry.js +64 -0
- package/dist/cjs/utils/retry.js.map +1 -0
- package/dist/cjs/voice-stream.js +184 -0
- package/dist/cjs/voice-stream.js.map +1 -0
- package/dist/client.d.ts +643 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +1280 -0
- package/dist/client.js.map +1 -0
- package/dist/constants.d.ts +141 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +176 -0
- package/dist/constants.js.map +1 -0
- package/dist/errors.d.ts +81 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +116 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/types/account.d.ts +161 -0
- package/dist/types/account.d.ts.map +1 -0
- package/dist/types/account.js +5 -0
- package/dist/types/account.js.map +1 -0
- package/dist/types/analysis.d.ts +41 -0
- package/dist/types/analysis.d.ts.map +1 -0
- package/dist/types/analysis.js +4 -0
- package/dist/types/analysis.js.map +1 -0
- package/dist/types/guidance.d.ts +35 -0
- package/dist/types/guidance.d.ts.map +1 -0
- package/dist/types/guidance.js +2 -0
- package/dist/types/guidance.js.map +1 -0
- package/dist/types/index.d.ts +231 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +12 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/media.d.ts +121 -0
- package/dist/types/media.d.ts.map +1 -0
- package/dist/types/media.js +4 -0
- package/dist/types/media.js.map +1 -0
- package/dist/types/policy.d.ts +54 -0
- package/dist/types/policy.d.ts.map +1 -0
- package/dist/types/policy.js +5 -0
- package/dist/types/policy.js.map +1 -0
- package/dist/types/pricing.d.ts +53 -0
- package/dist/types/pricing.d.ts.map +1 -0
- package/dist/types/pricing.js +5 -0
- package/dist/types/pricing.js.map +1 -0
- package/dist/types/reports.d.ts +44 -0
- package/dist/types/reports.d.ts.map +1 -0
- package/dist/types/reports.js +2 -0
- package/dist/types/reports.js.map +1 -0
- package/dist/types/safety.d.ts +151 -0
- package/dist/types/safety.d.ts.map +1 -0
- package/dist/types/safety.js +4 -0
- package/dist/types/safety.js.map +1 -0
- package/dist/types/voice-stream.d.ts +97 -0
- package/dist/types/voice-stream.d.ts.map +1 -0
- package/dist/types/voice-stream.js +5 -0
- package/dist/types/voice-stream.js.map +1 -0
- package/dist/types/webhooks.d.ts +110 -0
- package/dist/types/webhooks.d.ts.map +1 -0
- package/dist/types/webhooks.js +4 -0
- package/dist/types/webhooks.js.map +1 -0
- package/dist/utils/retry.d.ts +17 -0
- package/dist/utils/retry.d.ts.map +1 -0
- package/dist/utils/retry.js +61 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/voice-stream.d.ts +16 -0
- package/dist/voice-stream.d.ts.map +1 -0
- package/dist/voice-stream.js +148 -0
- package/dist/voice-stream.js.map +1 -0
- package/package.json +81 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,1280 @@
|
|
|
1
|
+
import { TuteliqError, AuthenticationError, RateLimitError, ValidationError, NotFoundError, ServerError, TimeoutError, NetworkError, QuotaExceededError, TierAccessError, } from './errors.js';
|
|
2
|
+
import { withRetry } from './utils/retry.js';
|
|
3
|
+
import { createVoiceStream } from './voice-stream.js';
|
|
4
|
+
/** Tuteliq API endpoint - locked to official server */
|
|
5
|
+
const API_BASE_URL = 'https://api.tuteliq.ai';
|
|
6
|
+
const DEFAULT_TIMEOUT = 30000;
|
|
7
|
+
const DEFAULT_RETRIES = 3;
|
|
8
|
+
const DEFAULT_RETRY_DELAY = 1000;
|
|
9
|
+
// Input limits to prevent abuse
|
|
10
|
+
const MAX_CONTENT_LENGTH = 50000; // 50KB max content
|
|
11
|
+
const MAX_MESSAGES_COUNT = 100; // Max messages per request
|
|
12
|
+
/**
|
|
13
|
+
* Tuteliq - AI-powered child safety analysis
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* import { Tuteliq } from '@tuteliq/sdk'
|
|
18
|
+
*
|
|
19
|
+
* const tuteliq = new Tuteliq(process.env.TUTELIQ_API_KEY)
|
|
20
|
+
*
|
|
21
|
+
* // Detect bullying
|
|
22
|
+
* const result = await tuteliq.detectBullying({
|
|
23
|
+
* content: "You're not welcome here",
|
|
24
|
+
* context: 'chat'
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* if (result.is_bullying) {
|
|
28
|
+
* console.log('Severity:', result.severity)
|
|
29
|
+
* console.log('Rationale:', result.rationale)
|
|
30
|
+
* }
|
|
31
|
+
*
|
|
32
|
+
* // Check usage
|
|
33
|
+
* console.log(tuteliq.usage) // { limit: 10000, used: 5234, remaining: 4766 }
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
export class Tuteliq {
|
|
37
|
+
apiKey;
|
|
38
|
+
timeout;
|
|
39
|
+
retries;
|
|
40
|
+
retryDelay;
|
|
41
|
+
_usage = null;
|
|
42
|
+
_rateLimit = null;
|
|
43
|
+
_lastRequestId = null;
|
|
44
|
+
_lastLatencyMs = null;
|
|
45
|
+
_usageWarning = null;
|
|
46
|
+
/**
|
|
47
|
+
* Create a new Tuteliq client
|
|
48
|
+
*
|
|
49
|
+
* @param apiKey - Your Tuteliq API key
|
|
50
|
+
* @param options - Optional configuration
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```typescript
|
|
54
|
+
* // Simple usage
|
|
55
|
+
* const tuteliq = new Tuteliq('your-api-key')
|
|
56
|
+
*
|
|
57
|
+
* // With options
|
|
58
|
+
* const tuteliq = new Tuteliq('your-api-key', {
|
|
59
|
+
* timeout: 10000
|
|
60
|
+
* })
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
constructor(apiKey, options = {}) {
|
|
64
|
+
if (!apiKey || typeof apiKey !== 'string') {
|
|
65
|
+
throw new Error('API key is required and must be a string');
|
|
66
|
+
}
|
|
67
|
+
if (apiKey.length < 10) {
|
|
68
|
+
throw new Error('API key appears to be invalid (too short)');
|
|
69
|
+
}
|
|
70
|
+
this.apiKey = apiKey;
|
|
71
|
+
this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
|
|
72
|
+
this.retries = options.retries ?? DEFAULT_RETRIES;
|
|
73
|
+
this.retryDelay = options.retryDelay ?? DEFAULT_RETRY_DELAY;
|
|
74
|
+
// Validate configuration
|
|
75
|
+
if (this.timeout < 1000 || this.timeout > 120000) {
|
|
76
|
+
throw new Error('Timeout must be between 1000ms and 120000ms');
|
|
77
|
+
}
|
|
78
|
+
if (this.retries < 0 || this.retries > 10) {
|
|
79
|
+
throw new Error('Retries must be between 0 and 10');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get current monthly usage stats from the last request
|
|
84
|
+
*/
|
|
85
|
+
get usage() {
|
|
86
|
+
return this._usage;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Get rate limit info from the last request (per-minute limits)
|
|
90
|
+
*/
|
|
91
|
+
get rateLimit() {
|
|
92
|
+
return this._rateLimit;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get usage warning message if usage is above 80%
|
|
96
|
+
*/
|
|
97
|
+
get usageWarning() {
|
|
98
|
+
return this._usageWarning;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Get the request ID from the last request
|
|
102
|
+
*/
|
|
103
|
+
get lastRequestId() {
|
|
104
|
+
return this._lastRequestId;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get the latency from the last request in milliseconds
|
|
108
|
+
*/
|
|
109
|
+
get lastLatencyMs() {
|
|
110
|
+
return this._lastLatencyMs;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Validate content length to prevent abuse
|
|
114
|
+
*/
|
|
115
|
+
validateContent(content) {
|
|
116
|
+
if (!content || typeof content !== 'string') {
|
|
117
|
+
throw new ValidationError('Content is required and must be a string');
|
|
118
|
+
}
|
|
119
|
+
if (content.length > MAX_CONTENT_LENGTH) {
|
|
120
|
+
throw new ValidationError(`Content exceeds maximum length of ${MAX_CONTENT_LENGTH} characters`);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Validate messages array
|
|
125
|
+
*/
|
|
126
|
+
validateMessages(messages) {
|
|
127
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
128
|
+
throw new ValidationError('Messages array is required and cannot be empty');
|
|
129
|
+
}
|
|
130
|
+
if (messages.length > MAX_MESSAGES_COUNT) {
|
|
131
|
+
throw new ValidationError(`Messages array exceeds maximum count of ${MAX_MESSAGES_COUNT}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
static SDK_IDENTIFIER = 'Node SDK';
|
|
135
|
+
/**
|
|
136
|
+
* Resolves the platform string by appending the SDK identifier.
|
|
137
|
+
* - "MyApp" → "MyApp - Node SDK"
|
|
138
|
+
* - undefined → "Node SDK"
|
|
139
|
+
*/
|
|
140
|
+
static resolvePlatform(platform) {
|
|
141
|
+
if (platform && platform.length > 0) {
|
|
142
|
+
return `${platform} - ${Tuteliq.SDK_IDENTIFIER}`;
|
|
143
|
+
}
|
|
144
|
+
return Tuteliq.SDK_IDENTIFIER;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Normalize context input to API format
|
|
148
|
+
*/
|
|
149
|
+
normalizeContext(context) {
|
|
150
|
+
if (!context)
|
|
151
|
+
return { platform: Tuteliq.resolvePlatform() };
|
|
152
|
+
if (typeof context === 'string') {
|
|
153
|
+
return { platform: Tuteliq.resolvePlatform(context) };
|
|
154
|
+
}
|
|
155
|
+
return { ...context, platform: Tuteliq.resolvePlatform(context.platform) };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Make an authenticated request to the API
|
|
159
|
+
*/
|
|
160
|
+
async request(method, path, body) {
|
|
161
|
+
const url = `${API_BASE_URL}${path}`;
|
|
162
|
+
const startTime = Date.now();
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
method,
|
|
168
|
+
headers: {
|
|
169
|
+
'Content-Type': 'application/json',
|
|
170
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
171
|
+
},
|
|
172
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
173
|
+
signal: controller.signal,
|
|
174
|
+
});
|
|
175
|
+
clearTimeout(timeoutId);
|
|
176
|
+
this._lastLatencyMs = Date.now() - startTime;
|
|
177
|
+
// Extract metadata from headers
|
|
178
|
+
this._lastRequestId = response.headers.get('x-request-id');
|
|
179
|
+
// Monthly usage headers (X-Monthly-*)
|
|
180
|
+
const monthlyLimit = response.headers.get('x-monthly-limit');
|
|
181
|
+
const monthlyUsed = response.headers.get('x-monthly-used');
|
|
182
|
+
const monthlyRemaining = response.headers.get('x-monthly-remaining');
|
|
183
|
+
if (monthlyLimit && monthlyUsed && monthlyRemaining) {
|
|
184
|
+
this._usage = {
|
|
185
|
+
limit: parseInt(monthlyLimit, 10),
|
|
186
|
+
used: parseInt(monthlyUsed, 10),
|
|
187
|
+
remaining: parseInt(monthlyRemaining, 10),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
// Rate limit headers (X-RateLimit-*)
|
|
191
|
+
const rateLimitLimit = response.headers.get('x-ratelimit-limit');
|
|
192
|
+
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
193
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
194
|
+
if (rateLimitLimit && rateLimitRemaining) {
|
|
195
|
+
this._rateLimit = {
|
|
196
|
+
limit: parseInt(rateLimitLimit, 10),
|
|
197
|
+
remaining: parseInt(rateLimitRemaining, 10),
|
|
198
|
+
reset: rateLimitReset ? parseInt(rateLimitReset, 10) : undefined,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
// Usage warning header
|
|
202
|
+
this._usageWarning = response.headers.get('x-usage-warning');
|
|
203
|
+
// Handle error responses
|
|
204
|
+
if (!response.ok) {
|
|
205
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
206
|
+
this.handleErrorResponse(response.status, errorBody, response.headers);
|
|
207
|
+
}
|
|
208
|
+
return await response.json();
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
clearTimeout(timeoutId);
|
|
212
|
+
this._lastLatencyMs = Date.now() - startTime;
|
|
213
|
+
if (error instanceof TuteliqError) {
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
if (error instanceof Error) {
|
|
217
|
+
if (error.name === 'AbortError') {
|
|
218
|
+
throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
|
|
219
|
+
}
|
|
220
|
+
// Detect network errors across runtimes (Node, browsers, edge)
|
|
221
|
+
if (error instanceof TypeError ||
|
|
222
|
+
error.name === 'TypeError' ||
|
|
223
|
+
error.message.includes('fetch') ||
|
|
224
|
+
error.message.includes('network') ||
|
|
225
|
+
error.message.includes('ECONNREFUSED') ||
|
|
226
|
+
error.message.includes('ECONNRESET') ||
|
|
227
|
+
error.message.includes('ENOTFOUND') ||
|
|
228
|
+
error.message.includes('ERR_NETWORK') ||
|
|
229
|
+
error.message.includes('Failed to fetch') ||
|
|
230
|
+
error.message.includes('Network request failed')) {
|
|
231
|
+
throw new NetworkError(error.message);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw new TuteliqError(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Handle error responses from the API
|
|
239
|
+
*/
|
|
240
|
+
handleErrorResponse(status, body, headers) {
|
|
241
|
+
const message = body.error?.message || 'Unknown error';
|
|
242
|
+
const code = body.error?.code;
|
|
243
|
+
const details = body.error?.details;
|
|
244
|
+
const suggestion = body.error?.suggestion;
|
|
245
|
+
const links = body.error?.links;
|
|
246
|
+
switch (status) {
|
|
247
|
+
case 400:
|
|
248
|
+
throw new ValidationError(message, details, { code, suggestion, links });
|
|
249
|
+
case 401:
|
|
250
|
+
throw new AuthenticationError(message, { code, suggestion, links });
|
|
251
|
+
case 403:
|
|
252
|
+
throw new TierAccessError(message, { code, suggestion, links });
|
|
253
|
+
case 404:
|
|
254
|
+
throw new NotFoundError(message, { code, suggestion, links });
|
|
255
|
+
case 429: {
|
|
256
|
+
const retryAfter = headers.get('retry-after');
|
|
257
|
+
if (code === 'RATE_2003' || code === 'QUOTA_EXCEEDED') {
|
|
258
|
+
throw new QuotaExceededError(message, { code, suggestion, links });
|
|
259
|
+
}
|
|
260
|
+
throw new RateLimitError(message, retryAfter ? parseInt(retryAfter, 10) : undefined, { code, suggestion, links });
|
|
261
|
+
}
|
|
262
|
+
default:
|
|
263
|
+
if (status >= 500) {
|
|
264
|
+
throw new ServerError(message, status, { code, suggestion, links });
|
|
265
|
+
}
|
|
266
|
+
throw new TuteliqError(message, status, details, { code, suggestion, links });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Make a request with retry logic
|
|
271
|
+
*/
|
|
272
|
+
async requestWithRetry(method, path, body) {
|
|
273
|
+
return withRetry(() => this.request(method, path, body), {
|
|
274
|
+
maxRetries: this.retries,
|
|
275
|
+
initialDelay: this.retryDelay,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Make an authenticated multipart request to the API
|
|
280
|
+
*/
|
|
281
|
+
async multipartRequest(path, formData) {
|
|
282
|
+
const url = `${API_BASE_URL}${path}`;
|
|
283
|
+
const startTime = Date.now();
|
|
284
|
+
const controller = new AbortController();
|
|
285
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeout);
|
|
286
|
+
try {
|
|
287
|
+
const response = await fetch(url, {
|
|
288
|
+
method: 'POST',
|
|
289
|
+
headers: {
|
|
290
|
+
'Authorization': `Bearer ${this.apiKey}`,
|
|
291
|
+
},
|
|
292
|
+
body: formData,
|
|
293
|
+
signal: controller.signal,
|
|
294
|
+
});
|
|
295
|
+
clearTimeout(timeoutId);
|
|
296
|
+
this._lastLatencyMs = Date.now() - startTime;
|
|
297
|
+
// Extract metadata from headers
|
|
298
|
+
this._lastRequestId = response.headers.get('x-request-id');
|
|
299
|
+
const monthlyLimit = response.headers.get('x-monthly-limit');
|
|
300
|
+
const monthlyUsed = response.headers.get('x-monthly-used');
|
|
301
|
+
const monthlyRemaining = response.headers.get('x-monthly-remaining');
|
|
302
|
+
if (monthlyLimit && monthlyUsed && monthlyRemaining) {
|
|
303
|
+
this._usage = {
|
|
304
|
+
limit: parseInt(monthlyLimit, 10),
|
|
305
|
+
used: parseInt(monthlyUsed, 10),
|
|
306
|
+
remaining: parseInt(monthlyRemaining, 10),
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
const rateLimitLimit = response.headers.get('x-ratelimit-limit');
|
|
310
|
+
const rateLimitRemaining = response.headers.get('x-ratelimit-remaining');
|
|
311
|
+
const rateLimitReset = response.headers.get('x-ratelimit-reset');
|
|
312
|
+
if (rateLimitLimit && rateLimitRemaining) {
|
|
313
|
+
this._rateLimit = {
|
|
314
|
+
limit: parseInt(rateLimitLimit, 10),
|
|
315
|
+
remaining: parseInt(rateLimitRemaining, 10),
|
|
316
|
+
reset: rateLimitReset ? parseInt(rateLimitReset, 10) : undefined,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
this._usageWarning = response.headers.get('x-usage-warning');
|
|
320
|
+
if (!response.ok) {
|
|
321
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
322
|
+
this.handleErrorResponse(response.status, errorBody, response.headers);
|
|
323
|
+
}
|
|
324
|
+
return await response.json();
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
clearTimeout(timeoutId);
|
|
328
|
+
this._lastLatencyMs = Date.now() - startTime;
|
|
329
|
+
if (error instanceof TuteliqError) {
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
332
|
+
if (error instanceof Error) {
|
|
333
|
+
if (error.name === 'AbortError') {
|
|
334
|
+
throw new TimeoutError(`Request timed out after ${this.timeout}ms`);
|
|
335
|
+
}
|
|
336
|
+
if (error instanceof TypeError ||
|
|
337
|
+
error.name === 'TypeError' ||
|
|
338
|
+
error.message.includes('fetch') ||
|
|
339
|
+
error.message.includes('network') ||
|
|
340
|
+
error.message.includes('ECONNREFUSED') ||
|
|
341
|
+
error.message.includes('ECONNRESET') ||
|
|
342
|
+
error.message.includes('ENOTFOUND') ||
|
|
343
|
+
error.message.includes('ERR_NETWORK') ||
|
|
344
|
+
error.message.includes('Failed to fetch') ||
|
|
345
|
+
error.message.includes('Network request failed')) {
|
|
346
|
+
throw new NetworkError(error.message);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
throw new TuteliqError(error instanceof Error ? error.message : 'Unknown error occurred');
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
// =========================================================================
|
|
353
|
+
// Safety Detection Methods
|
|
354
|
+
// =========================================================================
|
|
355
|
+
/**
|
|
356
|
+
* Detect bullying in content
|
|
357
|
+
*
|
|
358
|
+
* @example
|
|
359
|
+
* ```typescript
|
|
360
|
+
* const result = await tuteliq.detectBullying({
|
|
361
|
+
* content: "Nobody likes you, loser",
|
|
362
|
+
* context: 'chat'
|
|
363
|
+
* })
|
|
364
|
+
*
|
|
365
|
+
* if (result.is_bullying && result.severity === 'high') {
|
|
366
|
+
* console.log('High severity bullying detected')
|
|
367
|
+
* console.log('Rationale:', result.rationale)
|
|
368
|
+
* }
|
|
369
|
+
* ```
|
|
370
|
+
*/
|
|
371
|
+
async detectBullying(input) {
|
|
372
|
+
this.validateContent(input.content);
|
|
373
|
+
return this.requestWithRetry('POST', '/api/v1/safety/bullying', {
|
|
374
|
+
text: input.content,
|
|
375
|
+
context: this.normalizeContext(input.context),
|
|
376
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
377
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
378
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Detect grooming patterns in a conversation
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```typescript
|
|
386
|
+
* const result = await tuteliq.detectGrooming({
|
|
387
|
+
* messages: [
|
|
388
|
+
* { role: 'adult', content: "Don't tell your parents" },
|
|
389
|
+
* { role: 'child', content: "Ok" }
|
|
390
|
+
* ],
|
|
391
|
+
* childAge: 12
|
|
392
|
+
* })
|
|
393
|
+
*
|
|
394
|
+
* if (result.grooming_risk === 'high') {
|
|
395
|
+
* console.log('Flags:', result.flags)
|
|
396
|
+
* }
|
|
397
|
+
* ```
|
|
398
|
+
*/
|
|
399
|
+
async detectGrooming(input) {
|
|
400
|
+
this.validateMessages(input.messages);
|
|
401
|
+
return this.requestWithRetry('POST', '/api/v1/safety/grooming', {
|
|
402
|
+
messages: input.messages.map(m => ({
|
|
403
|
+
sender_role: m.role,
|
|
404
|
+
text: m.content,
|
|
405
|
+
})),
|
|
406
|
+
context: {
|
|
407
|
+
child_age: input.childAge,
|
|
408
|
+
...this.normalizeContext(input.context),
|
|
409
|
+
},
|
|
410
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
411
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
412
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Detect unsafe content (self-harm, violence, hate speech, etc.)
|
|
417
|
+
*
|
|
418
|
+
* @example
|
|
419
|
+
* ```typescript
|
|
420
|
+
* const result = await tuteliq.detectUnsafe({
|
|
421
|
+
* content: "I want to hurt myself"
|
|
422
|
+
* })
|
|
423
|
+
*
|
|
424
|
+
* if (result.unsafe && result.categories.includes('self_harm')) {
|
|
425
|
+
* console.log('Show crisis resources')
|
|
426
|
+
* }
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
async detectUnsafe(input) {
|
|
430
|
+
this.validateContent(input.content);
|
|
431
|
+
return this.requestWithRetry('POST', '/api/v1/safety/unsafe', {
|
|
432
|
+
text: input.content,
|
|
433
|
+
context: this.normalizeContext(input.context),
|
|
434
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
435
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
436
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
async analyze(contentOrInput, context) {
|
|
440
|
+
const input = typeof contentOrInput === 'string'
|
|
441
|
+
? { content: contentOrInput, context }
|
|
442
|
+
: contentOrInput;
|
|
443
|
+
const include = input.include || ['bullying', 'unsafe'];
|
|
444
|
+
// Run detections in parallel
|
|
445
|
+
const promises = [];
|
|
446
|
+
const types = [];
|
|
447
|
+
if (include.includes('bullying')) {
|
|
448
|
+
types.push('bullying');
|
|
449
|
+
promises.push(this.detectBullying({
|
|
450
|
+
content: input.content,
|
|
451
|
+
context: input.context,
|
|
452
|
+
external_id: input.external_id,
|
|
453
|
+
customer_id: input.customer_id,
|
|
454
|
+
metadata: input.metadata,
|
|
455
|
+
}));
|
|
456
|
+
}
|
|
457
|
+
if (include.includes('unsafe')) {
|
|
458
|
+
types.push('unsafe');
|
|
459
|
+
promises.push(this.detectUnsafe({
|
|
460
|
+
content: input.content,
|
|
461
|
+
context: input.context,
|
|
462
|
+
external_id: input.external_id,
|
|
463
|
+
customer_id: input.customer_id,
|
|
464
|
+
metadata: input.metadata,
|
|
465
|
+
}));
|
|
466
|
+
}
|
|
467
|
+
const results = await Promise.all(promises);
|
|
468
|
+
// Combine results
|
|
469
|
+
let bullyingResult;
|
|
470
|
+
let unsafeResult;
|
|
471
|
+
let maxRiskScore = 0;
|
|
472
|
+
results.forEach((result, i) => {
|
|
473
|
+
if (types[i] === 'bullying') {
|
|
474
|
+
bullyingResult = result;
|
|
475
|
+
maxRiskScore = Math.max(maxRiskScore, bullyingResult.risk_score);
|
|
476
|
+
}
|
|
477
|
+
else if (types[i] === 'unsafe') {
|
|
478
|
+
unsafeResult = result;
|
|
479
|
+
maxRiskScore = Math.max(maxRiskScore, unsafeResult.risk_score);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
// Determine risk level
|
|
483
|
+
let risk_level = 'safe';
|
|
484
|
+
if (maxRiskScore >= 0.9)
|
|
485
|
+
risk_level = 'critical';
|
|
486
|
+
else if (maxRiskScore >= 0.7)
|
|
487
|
+
risk_level = 'high';
|
|
488
|
+
else if (maxRiskScore >= 0.5)
|
|
489
|
+
risk_level = 'medium';
|
|
490
|
+
else if (maxRiskScore >= 0.3)
|
|
491
|
+
risk_level = 'low';
|
|
492
|
+
// Build summary
|
|
493
|
+
const findings = [];
|
|
494
|
+
if (bullyingResult?.is_bullying) {
|
|
495
|
+
findings.push(`Bullying detected (${bullyingResult.severity})`);
|
|
496
|
+
}
|
|
497
|
+
if (unsafeResult?.unsafe) {
|
|
498
|
+
findings.push(`Unsafe content: ${unsafeResult.categories.join(', ')}`);
|
|
499
|
+
}
|
|
500
|
+
const summary = findings.length > 0
|
|
501
|
+
? findings.join('. ')
|
|
502
|
+
: 'No safety concerns detected.';
|
|
503
|
+
// Determine recommended action
|
|
504
|
+
let recommended_action = 'none';
|
|
505
|
+
if (bullyingResult?.recommended_action === 'immediate_intervention' ||
|
|
506
|
+
unsafeResult?.recommended_action === 'immediate_intervention') {
|
|
507
|
+
recommended_action = 'immediate_intervention';
|
|
508
|
+
}
|
|
509
|
+
else if (bullyingResult?.recommended_action === 'flag_for_moderator' ||
|
|
510
|
+
unsafeResult?.recommended_action === 'flag_for_moderator') {
|
|
511
|
+
recommended_action = 'flag_for_moderator';
|
|
512
|
+
}
|
|
513
|
+
else if (bullyingResult?.recommended_action === 'monitor' ||
|
|
514
|
+
unsafeResult?.recommended_action === 'monitor') {
|
|
515
|
+
recommended_action = 'monitor';
|
|
516
|
+
}
|
|
517
|
+
return {
|
|
518
|
+
risk_level,
|
|
519
|
+
risk_score: maxRiskScore,
|
|
520
|
+
summary,
|
|
521
|
+
bullying: bullyingResult,
|
|
522
|
+
unsafe: unsafeResult,
|
|
523
|
+
recommended_action,
|
|
524
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
525
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
526
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
// =========================================================================
|
|
530
|
+
// Analysis Methods
|
|
531
|
+
// =========================================================================
|
|
532
|
+
/**
|
|
533
|
+
* Analyze emotions in content or conversation
|
|
534
|
+
*
|
|
535
|
+
* @example
|
|
536
|
+
* ```typescript
|
|
537
|
+
* const result = await tuteliq.analyzeEmotions({
|
|
538
|
+
* content: "I'm so stressed about everything"
|
|
539
|
+
* })
|
|
540
|
+
*
|
|
541
|
+
* console.log('Emotions:', result.dominant_emotions)
|
|
542
|
+
* console.log('Trend:', result.trend)
|
|
543
|
+
* ```
|
|
544
|
+
*/
|
|
545
|
+
async analyzeEmotions(input) {
|
|
546
|
+
if (input.content) {
|
|
547
|
+
this.validateContent(input.content);
|
|
548
|
+
}
|
|
549
|
+
else if (input.messages) {
|
|
550
|
+
this.validateMessages(input.messages);
|
|
551
|
+
}
|
|
552
|
+
else {
|
|
553
|
+
throw new ValidationError('Either content or messages is required');
|
|
554
|
+
}
|
|
555
|
+
const body = {};
|
|
556
|
+
if (input.content) {
|
|
557
|
+
body.messages = [{ sender: 'user', text: input.content }];
|
|
558
|
+
}
|
|
559
|
+
else if (input.messages) {
|
|
560
|
+
body.messages = input.messages.map(m => ({
|
|
561
|
+
sender: m.sender,
|
|
562
|
+
text: m.content,
|
|
563
|
+
}));
|
|
564
|
+
}
|
|
565
|
+
if (input.context) {
|
|
566
|
+
body.context = this.normalizeContext(input.context);
|
|
567
|
+
}
|
|
568
|
+
if (input.external_id) {
|
|
569
|
+
body.external_id = input.external_id;
|
|
570
|
+
}
|
|
571
|
+
if (input.customer_id) {
|
|
572
|
+
body.customer_id = input.customer_id;
|
|
573
|
+
}
|
|
574
|
+
if (input.metadata) {
|
|
575
|
+
body.metadata = input.metadata;
|
|
576
|
+
}
|
|
577
|
+
return this.requestWithRetry('POST', '/api/v1/analysis/emotions', body);
|
|
578
|
+
}
|
|
579
|
+
// =========================================================================
|
|
580
|
+
// Guidance Methods
|
|
581
|
+
// =========================================================================
|
|
582
|
+
/**
|
|
583
|
+
* Get age-appropriate action guidance for a situation
|
|
584
|
+
*
|
|
585
|
+
* @example
|
|
586
|
+
* ```typescript
|
|
587
|
+
* const plan = await tuteliq.getActionPlan({
|
|
588
|
+
* situation: 'Someone is spreading rumors about me',
|
|
589
|
+
* childAge: 12,
|
|
590
|
+
* audience: 'child'
|
|
591
|
+
* })
|
|
592
|
+
*
|
|
593
|
+
* console.log('Steps:', plan.steps)
|
|
594
|
+
* ```
|
|
595
|
+
*/
|
|
596
|
+
async getActionPlan(input) {
|
|
597
|
+
if (!input.situation || typeof input.situation !== 'string') {
|
|
598
|
+
throw new ValidationError('Situation description is required');
|
|
599
|
+
}
|
|
600
|
+
this.validateContent(input.situation);
|
|
601
|
+
return this.requestWithRetry('POST', '/api/v1/guidance/action-plan', {
|
|
602
|
+
role: input.audience || 'parent',
|
|
603
|
+
situation: input.situation,
|
|
604
|
+
child_age: input.childAge,
|
|
605
|
+
severity: input.severity,
|
|
606
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
607
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
608
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
// =========================================================================
|
|
612
|
+
// Report Methods
|
|
613
|
+
// =========================================================================
|
|
614
|
+
/**
|
|
615
|
+
* Generate an incident report from messages
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```typescript
|
|
619
|
+
* const report = await tuteliq.generateReport({
|
|
620
|
+
* messages: [
|
|
621
|
+
* { sender: 'user1', content: 'Harmful message' },
|
|
622
|
+
* { sender: 'child', content: 'Response' }
|
|
623
|
+
* ],
|
|
624
|
+
* childAge: 14
|
|
625
|
+
* })
|
|
626
|
+
*
|
|
627
|
+
* console.log('Summary:', report.summary)
|
|
628
|
+
* console.log('Risk:', report.risk_level)
|
|
629
|
+
* console.log('Next steps:', report.recommended_next_steps)
|
|
630
|
+
* ```
|
|
631
|
+
*/
|
|
632
|
+
async generateReport(input) {
|
|
633
|
+
this.validateMessages(input.messages);
|
|
634
|
+
return this.requestWithRetry('POST', '/api/v1/reports/incident', {
|
|
635
|
+
messages: input.messages.map(m => ({
|
|
636
|
+
sender: m.sender,
|
|
637
|
+
text: m.content,
|
|
638
|
+
})),
|
|
639
|
+
meta: {
|
|
640
|
+
child_age: input.childAge,
|
|
641
|
+
...input.incident,
|
|
642
|
+
},
|
|
643
|
+
...(input.external_id && { external_id: input.external_id }),
|
|
644
|
+
...(input.customer_id && { customer_id: input.customer_id }),
|
|
645
|
+
...(input.metadata && { metadata: input.metadata }),
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// =========================================================================
|
|
649
|
+
// Policy Methods
|
|
650
|
+
// =========================================================================
|
|
651
|
+
/**
|
|
652
|
+
* Get the current policy configuration
|
|
653
|
+
*
|
|
654
|
+
* @example
|
|
655
|
+
* ```typescript
|
|
656
|
+
* const policy = await tuteliq.getPolicy()
|
|
657
|
+
* console.log('Bullying enabled:', policy.config?.bullying.enabled)
|
|
658
|
+
* ```
|
|
659
|
+
*/
|
|
660
|
+
async getPolicy() {
|
|
661
|
+
return this.requestWithRetry('GET', '/api/v1/policy');
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Update the policy configuration
|
|
665
|
+
*
|
|
666
|
+
* @example
|
|
667
|
+
* ```typescript
|
|
668
|
+
* await tuteliq.setPolicy({
|
|
669
|
+
* bullying: {
|
|
670
|
+
* enabled: true,
|
|
671
|
+
* minRiskScoreToFlag: 0.5
|
|
672
|
+
* }
|
|
673
|
+
* })
|
|
674
|
+
* ```
|
|
675
|
+
*/
|
|
676
|
+
async setPolicy(config) {
|
|
677
|
+
return this.requestWithRetry('PUT', '/api/v1/policy', { config });
|
|
678
|
+
}
|
|
679
|
+
// =========================================================================
|
|
680
|
+
// Batch Methods
|
|
681
|
+
// =========================================================================
|
|
682
|
+
/**
|
|
683
|
+
* Analyze multiple items in a single batch request
|
|
684
|
+
*
|
|
685
|
+
* @example
|
|
686
|
+
* ```typescript
|
|
687
|
+
* const result = await tuteliq.batch({
|
|
688
|
+
* items: [
|
|
689
|
+
* { type: 'bullying', content: 'Message 1' },
|
|
690
|
+
* { type: 'unsafe', content: 'Message 2' },
|
|
691
|
+
* ],
|
|
692
|
+
* parallel: true
|
|
693
|
+
* })
|
|
694
|
+
*
|
|
695
|
+
* console.log('Success:', result.summary.successful)
|
|
696
|
+
* console.log('Failed:', result.summary.failed)
|
|
697
|
+
* ```
|
|
698
|
+
*/
|
|
699
|
+
async batch(input) {
|
|
700
|
+
if (!input.items || input.items.length === 0) {
|
|
701
|
+
throw new ValidationError('Items array is required and cannot be empty');
|
|
702
|
+
}
|
|
703
|
+
if (input.items.length > 25) {
|
|
704
|
+
throw new ValidationError('Maximum 25 items per batch request');
|
|
705
|
+
}
|
|
706
|
+
return this.requestWithRetry('POST', '/api/v1/batch/analyze', {
|
|
707
|
+
items: input.items.map(item => {
|
|
708
|
+
if (item.type === 'grooming') {
|
|
709
|
+
return {
|
|
710
|
+
type: item.type,
|
|
711
|
+
messages: item.messages.map(m => ({
|
|
712
|
+
sender_role: m.role,
|
|
713
|
+
text: m.content,
|
|
714
|
+
})),
|
|
715
|
+
context: {
|
|
716
|
+
...(item.childAge != null && { child_age: item.childAge }),
|
|
717
|
+
...this.normalizeContext(item.context),
|
|
718
|
+
},
|
|
719
|
+
external_id: item.external_id,
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
return {
|
|
723
|
+
type: item.type,
|
|
724
|
+
text: item.content,
|
|
725
|
+
context: this.normalizeContext(item.context),
|
|
726
|
+
external_id: item.external_id,
|
|
727
|
+
};
|
|
728
|
+
}),
|
|
729
|
+
options: {
|
|
730
|
+
parallel: input.parallel ?? true,
|
|
731
|
+
continue_on_error: input.continueOnError ?? true,
|
|
732
|
+
},
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
// =========================================================================
|
|
736
|
+
// Usage Methods
|
|
737
|
+
// =========================================================================
|
|
738
|
+
/**
|
|
739
|
+
* Get usage summary for the current billing period
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* const summary = await tuteliq.getUsageSummary()
|
|
744
|
+
* console.log('Used:', summary.messages_used)
|
|
745
|
+
* console.log('Limit:', summary.message_limit)
|
|
746
|
+
* console.log('Percent:', summary.usage_percentage)
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
async getUsageSummary() {
|
|
750
|
+
return this.requestWithRetry('GET', '/api/v1/usage/summary');
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* Get current rate limit quota status
|
|
754
|
+
*
|
|
755
|
+
* @example
|
|
756
|
+
* ```typescript
|
|
757
|
+
* const quota = await tuteliq.getQuota()
|
|
758
|
+
* console.log('Rate limit:', quota.rate_limit)
|
|
759
|
+
* console.log('Remaining this minute:', quota.remaining)
|
|
760
|
+
* ```
|
|
761
|
+
*/
|
|
762
|
+
async getQuota() {
|
|
763
|
+
return this.requestWithRetry('GET', '/api/v1/usage/quota');
|
|
764
|
+
}
|
|
765
|
+
// =========================================================================
|
|
766
|
+
// Account Management (GDPR)
|
|
767
|
+
// =========================================================================
|
|
768
|
+
/**
|
|
769
|
+
* Delete all data associated with your account (GDPR Article 17 — Right to Erasure)
|
|
770
|
+
*
|
|
771
|
+
* This permanently deletes all user data including API keys, usage logs,
|
|
772
|
+
* incidents, emotional records, grooming assessments, and safety goals.
|
|
773
|
+
*
|
|
774
|
+
* @example
|
|
775
|
+
* ```typescript
|
|
776
|
+
* const result = await tuteliq.deleteAccountData()
|
|
777
|
+
* console.log(result.message) // "All user data has been deleted"
|
|
778
|
+
* console.log(result.deleted_count) // 42
|
|
779
|
+
* ```
|
|
780
|
+
*/
|
|
781
|
+
async deleteAccountData() {
|
|
782
|
+
return this.requestWithRetry('DELETE', '/api/v1/account/data');
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Export all data associated with your account (GDPR Article 20 — Right to Data Portability)
|
|
786
|
+
*
|
|
787
|
+
* Returns a JSON export of all stored data grouped by collection.
|
|
788
|
+
*
|
|
789
|
+
* @example
|
|
790
|
+
* ```typescript
|
|
791
|
+
* const data = await tuteliq.exportAccountData()
|
|
792
|
+
* console.log(data.userId)
|
|
793
|
+
* console.log(data.exportedAt)
|
|
794
|
+
* console.log(Object.keys(data.data)) // ['api_keys', 'incidents', ...]
|
|
795
|
+
* ```
|
|
796
|
+
*/
|
|
797
|
+
async exportAccountData() {
|
|
798
|
+
return this.requestWithRetry('GET', '/api/v1/account/export');
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Record user consent (GDPR Article 7)
|
|
802
|
+
*
|
|
803
|
+
* Creates an immutable consent record for audit trail.
|
|
804
|
+
*
|
|
805
|
+
* @example
|
|
806
|
+
* ```typescript
|
|
807
|
+
* const result = await tuteliq.recordConsent({
|
|
808
|
+
* consent_type: 'child_safety_monitoring',
|
|
809
|
+
* version: '1.0'
|
|
810
|
+
* })
|
|
811
|
+
* ```
|
|
812
|
+
*/
|
|
813
|
+
async recordConsent(input) {
|
|
814
|
+
return this.requestWithRetry('POST', '/api/v1/account/consent', input);
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Get current consent status (GDPR Article 7)
|
|
818
|
+
*
|
|
819
|
+
* Returns the latest consent record per type.
|
|
820
|
+
*
|
|
821
|
+
* @example
|
|
822
|
+
* ```typescript
|
|
823
|
+
* // Get all consent statuses
|
|
824
|
+
* const all = await tuteliq.getConsentStatus()
|
|
825
|
+
*
|
|
826
|
+
* // Get specific consent type
|
|
827
|
+
* const monitoring = await tuteliq.getConsentStatus('child_safety_monitoring')
|
|
828
|
+
* ```
|
|
829
|
+
*/
|
|
830
|
+
async getConsentStatus(type) {
|
|
831
|
+
const query = type ? `?type=${type}` : '';
|
|
832
|
+
return this.requestWithRetry('GET', `/api/v1/account/consent${query}`);
|
|
833
|
+
}
|
|
834
|
+
/**
|
|
835
|
+
* Withdraw consent (GDPR Article 7.3)
|
|
836
|
+
*
|
|
837
|
+
* Creates a withdrawal record. Does not delete consent history.
|
|
838
|
+
*
|
|
839
|
+
* @example
|
|
840
|
+
* ```typescript
|
|
841
|
+
* const result = await tuteliq.withdrawConsent('marketing')
|
|
842
|
+
* ```
|
|
843
|
+
*/
|
|
844
|
+
async withdrawConsent(type) {
|
|
845
|
+
return this.requestWithRetry('DELETE', `/api/v1/account/consent/${type}`);
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Rectify user data (GDPR Article 16 — Right to Rectification)
|
|
849
|
+
*
|
|
850
|
+
* Updates allowlisted fields on a specific document.
|
|
851
|
+
*
|
|
852
|
+
* @example
|
|
853
|
+
* ```typescript
|
|
854
|
+
* const result = await tuteliq.rectifyData({
|
|
855
|
+
* collection: 'incidents',
|
|
856
|
+
* document_id: 'abc123',
|
|
857
|
+
* fields: { summary: 'Corrected summary' }
|
|
858
|
+
* })
|
|
859
|
+
* ```
|
|
860
|
+
*/
|
|
861
|
+
async rectifyData(input) {
|
|
862
|
+
return this.requestWithRetry('PATCH', '/api/v1/account/data', input);
|
|
863
|
+
}
|
|
864
|
+
/**
|
|
865
|
+
* Get audit logs (GDPR Article 15 — Right of Access)
|
|
866
|
+
*
|
|
867
|
+
* Returns the user's audit trail of all data operations.
|
|
868
|
+
*
|
|
869
|
+
* @example
|
|
870
|
+
* ```typescript
|
|
871
|
+
* // Get all audit logs
|
|
872
|
+
* const logs = await tuteliq.getAuditLogs()
|
|
873
|
+
*
|
|
874
|
+
* // Filter by action
|
|
875
|
+
* const exports = await tuteliq.getAuditLogs({ action: 'data_export', limit: 10 })
|
|
876
|
+
* ```
|
|
877
|
+
*/
|
|
878
|
+
async getAuditLogs(options) {
|
|
879
|
+
const params = new URLSearchParams();
|
|
880
|
+
if (options?.action)
|
|
881
|
+
params.set('action', options.action);
|
|
882
|
+
if (options?.limit)
|
|
883
|
+
params.set('limit', String(options.limit));
|
|
884
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
885
|
+
return this.requestWithRetry('GET', `/api/v1/account/audit-logs${query}`);
|
|
886
|
+
}
|
|
887
|
+
// =========================================================================
|
|
888
|
+
// Breach Management (GDPR Article 33/34)
|
|
889
|
+
// =========================================================================
|
|
890
|
+
/**
|
|
891
|
+
* Log a new data breach
|
|
892
|
+
*
|
|
893
|
+
* @example
|
|
894
|
+
* ```typescript
|
|
895
|
+
* const result = await tuteliq.logBreach({
|
|
896
|
+
* title: 'Unauthorized access to user data',
|
|
897
|
+
* description: 'A third-party service exposed user emails',
|
|
898
|
+
* severity: 'high',
|
|
899
|
+
* affected_user_ids: ['user-1', 'user-2'],
|
|
900
|
+
* data_categories: ['email', 'name'],
|
|
901
|
+
* reported_by: 'security-team'
|
|
902
|
+
* })
|
|
903
|
+
* ```
|
|
904
|
+
*/
|
|
905
|
+
async logBreach(input) {
|
|
906
|
+
return this.requestWithRetry('POST', '/api/v1/admin/breach', input);
|
|
907
|
+
}
|
|
908
|
+
/**
|
|
909
|
+
* List data breaches
|
|
910
|
+
*
|
|
911
|
+
* @example
|
|
912
|
+
* ```typescript
|
|
913
|
+
* // List all breaches
|
|
914
|
+
* const all = await tuteliq.listBreaches()
|
|
915
|
+
*
|
|
916
|
+
* // Filter by status
|
|
917
|
+
* const active = await tuteliq.listBreaches({ status: 'investigating', limit: 10 })
|
|
918
|
+
* ```
|
|
919
|
+
*/
|
|
920
|
+
async listBreaches(options) {
|
|
921
|
+
const params = new URLSearchParams();
|
|
922
|
+
if (options?.status)
|
|
923
|
+
params.set('status', options.status);
|
|
924
|
+
if (options?.limit)
|
|
925
|
+
params.set('limit', String(options.limit));
|
|
926
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
927
|
+
return this.requestWithRetry('GET', `/api/v1/admin/breach${query}`);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get a single breach by ID
|
|
931
|
+
*
|
|
932
|
+
* @example
|
|
933
|
+
* ```typescript
|
|
934
|
+
* const result = await tuteliq.getBreach('breach-123')
|
|
935
|
+
* console.log(result.breach.status)
|
|
936
|
+
* ```
|
|
937
|
+
*/
|
|
938
|
+
async getBreach(id) {
|
|
939
|
+
return this.requestWithRetry('GET', `/api/v1/admin/breach/${id}`);
|
|
940
|
+
}
|
|
941
|
+
/**
|
|
942
|
+
* Update a breach's status and notification status
|
|
943
|
+
*
|
|
944
|
+
* @example
|
|
945
|
+
* ```typescript
|
|
946
|
+
* const result = await tuteliq.updateBreachStatus('breach-123', {
|
|
947
|
+
* status: 'contained',
|
|
948
|
+
* notification_status: 'users_notified',
|
|
949
|
+
* notes: 'All affected users have been notified via email'
|
|
950
|
+
* })
|
|
951
|
+
* ```
|
|
952
|
+
*/
|
|
953
|
+
async updateBreachStatus(id, input) {
|
|
954
|
+
return this.requestWithRetry('PATCH', `/api/v1/admin/breach/${id}`, input);
|
|
955
|
+
}
|
|
956
|
+
// =========================================================================
|
|
957
|
+
// Media Analysis Methods
|
|
958
|
+
// =========================================================================
|
|
959
|
+
/**
|
|
960
|
+
* Analyze voice/audio for safety concerns
|
|
961
|
+
*
|
|
962
|
+
* Transcribes the audio via Whisper, then runs safety analysis on the transcript.
|
|
963
|
+
*
|
|
964
|
+
* @example
|
|
965
|
+
* ```typescript
|
|
966
|
+
* import { readFileSync } from 'fs'
|
|
967
|
+
*
|
|
968
|
+
* const result = await tuteliq.analyzeVoice({
|
|
969
|
+
* file: readFileSync('recording.mp3'),
|
|
970
|
+
* filename: 'recording.mp3',
|
|
971
|
+
* analysisType: 'all'
|
|
972
|
+
* })
|
|
973
|
+
*
|
|
974
|
+
* console.log('Transcript:', result.transcription.text)
|
|
975
|
+
* console.log('Risk:', result.overall_severity)
|
|
976
|
+
* ```
|
|
977
|
+
*/
|
|
978
|
+
async analyzeVoice(input) {
|
|
979
|
+
if (!input.file) {
|
|
980
|
+
throw new ValidationError('Audio file is required');
|
|
981
|
+
}
|
|
982
|
+
if (!input.filename) {
|
|
983
|
+
throw new ValidationError('Filename is required');
|
|
984
|
+
}
|
|
985
|
+
const formData = new FormData();
|
|
986
|
+
if (Buffer.isBuffer(input.file)) {
|
|
987
|
+
formData.append('file', new Blob([input.file]), input.filename);
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
formData.append('file', input.file, input.filename);
|
|
991
|
+
}
|
|
992
|
+
if (input.analysisType)
|
|
993
|
+
formData.append('analysis_type', input.analysisType);
|
|
994
|
+
if (input.fileId)
|
|
995
|
+
formData.append('file_id', input.fileId);
|
|
996
|
+
if (input.external_id)
|
|
997
|
+
formData.append('external_id', input.external_id);
|
|
998
|
+
if (input.customer_id)
|
|
999
|
+
formData.append('customer_id', input.customer_id);
|
|
1000
|
+
if (input.ageGroup)
|
|
1001
|
+
formData.append('age_group', input.ageGroup);
|
|
1002
|
+
if (input.language)
|
|
1003
|
+
formData.append('language', input.language);
|
|
1004
|
+
formData.append('platform', Tuteliq.resolvePlatform(input.platform));
|
|
1005
|
+
if (input.childAge != null)
|
|
1006
|
+
formData.append('child_age', String(input.childAge));
|
|
1007
|
+
if (input.metadata)
|
|
1008
|
+
formData.append('metadata', JSON.stringify(input.metadata));
|
|
1009
|
+
return withRetry(() => this.multipartRequest('/api/v1/safety/voice', formData), { maxRetries: this.retries, initialDelay: this.retryDelay });
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Analyze an image for safety concerns
|
|
1013
|
+
*
|
|
1014
|
+
* Uses vision AI for visual content classification and OCR text extraction,
|
|
1015
|
+
* then runs safety analysis on any extracted text.
|
|
1016
|
+
*
|
|
1017
|
+
* @example
|
|
1018
|
+
* ```typescript
|
|
1019
|
+
* import { readFileSync } from 'fs'
|
|
1020
|
+
*
|
|
1021
|
+
* const result = await tuteliq.analyzeImage({
|
|
1022
|
+
* file: readFileSync('screenshot.png'),
|
|
1023
|
+
* filename: 'screenshot.png',
|
|
1024
|
+
* analysisType: 'all'
|
|
1025
|
+
* })
|
|
1026
|
+
*
|
|
1027
|
+
* console.log('Visual:', result.vision.visual_description)
|
|
1028
|
+
* console.log('OCR text:', result.vision.extracted_text)
|
|
1029
|
+
* console.log('Risk:', result.overall_severity)
|
|
1030
|
+
* ```
|
|
1031
|
+
*/
|
|
1032
|
+
async analyzeImage(input) {
|
|
1033
|
+
if (!input.file) {
|
|
1034
|
+
throw new ValidationError('Image file is required');
|
|
1035
|
+
}
|
|
1036
|
+
if (!input.filename) {
|
|
1037
|
+
throw new ValidationError('Filename is required');
|
|
1038
|
+
}
|
|
1039
|
+
const formData = new FormData();
|
|
1040
|
+
if (Buffer.isBuffer(input.file)) {
|
|
1041
|
+
formData.append('file', new Blob([input.file]), input.filename);
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
formData.append('file', input.file, input.filename);
|
|
1045
|
+
}
|
|
1046
|
+
if (input.analysisType)
|
|
1047
|
+
formData.append('analysis_type', input.analysisType);
|
|
1048
|
+
if (input.fileId)
|
|
1049
|
+
formData.append('file_id', input.fileId);
|
|
1050
|
+
if (input.external_id)
|
|
1051
|
+
formData.append('external_id', input.external_id);
|
|
1052
|
+
if (input.customer_id)
|
|
1053
|
+
formData.append('customer_id', input.customer_id);
|
|
1054
|
+
if (input.ageGroup)
|
|
1055
|
+
formData.append('age_group', input.ageGroup);
|
|
1056
|
+
formData.append('platform', Tuteliq.resolvePlatform(input.platform));
|
|
1057
|
+
if (input.metadata)
|
|
1058
|
+
formData.append('metadata', JSON.stringify(input.metadata));
|
|
1059
|
+
return withRetry(() => this.multipartRequest('/api/v1/safety/image', formData), { maxRetries: this.retries, initialDelay: this.retryDelay });
|
|
1060
|
+
}
|
|
1061
|
+
// =========================================================================
|
|
1062
|
+
// Webhook Methods
|
|
1063
|
+
// =========================================================================
|
|
1064
|
+
/**
|
|
1065
|
+
* List all webhooks for your account
|
|
1066
|
+
*
|
|
1067
|
+
* @example
|
|
1068
|
+
* ```typescript
|
|
1069
|
+
* const { webhooks } = await tuteliq.listWebhooks()
|
|
1070
|
+
* webhooks.forEach(w => console.log(w.name, w.is_active))
|
|
1071
|
+
* ```
|
|
1072
|
+
*/
|
|
1073
|
+
async listWebhooks() {
|
|
1074
|
+
return this.requestWithRetry('GET', '/api/v1/webhooks');
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Create a new webhook
|
|
1078
|
+
*
|
|
1079
|
+
* The returned `secret` is only shown once — store it securely for
|
|
1080
|
+
* signature verification.
|
|
1081
|
+
*
|
|
1082
|
+
* @example
|
|
1083
|
+
* ```typescript
|
|
1084
|
+
* import { WebhookEventType } from '@tuteliq/sdk'
|
|
1085
|
+
*
|
|
1086
|
+
* const result = await tuteliq.createWebhook({
|
|
1087
|
+
* name: 'Safety Alerts',
|
|
1088
|
+
* url: 'https://example.com/webhooks/tuteliq',
|
|
1089
|
+
* events: [WebhookEventType.INCIDENT_CRITICAL, WebhookEventType.GROOMING_DETECTED]
|
|
1090
|
+
* })
|
|
1091
|
+
*
|
|
1092
|
+
* console.log('Secret:', result.secret) // Store this securely!
|
|
1093
|
+
* ```
|
|
1094
|
+
*/
|
|
1095
|
+
async createWebhook(input) {
|
|
1096
|
+
return this.requestWithRetry('POST', '/api/v1/webhooks', {
|
|
1097
|
+
name: input.name,
|
|
1098
|
+
url: input.url,
|
|
1099
|
+
events: input.events,
|
|
1100
|
+
...(input.headers && { headers: input.headers }),
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Update an existing webhook
|
|
1105
|
+
*
|
|
1106
|
+
* @example
|
|
1107
|
+
* ```typescript
|
|
1108
|
+
* const result = await tuteliq.updateWebhook('webhook-123', {
|
|
1109
|
+
* name: 'Updated Name',
|
|
1110
|
+
* isActive: false
|
|
1111
|
+
* })
|
|
1112
|
+
* ```
|
|
1113
|
+
*/
|
|
1114
|
+
async updateWebhook(id, input) {
|
|
1115
|
+
return this.requestWithRetry('PUT', `/api/v1/webhooks/${id}`, {
|
|
1116
|
+
...(input.name !== undefined && { name: input.name }),
|
|
1117
|
+
...(input.url !== undefined && { url: input.url }),
|
|
1118
|
+
...(input.events !== undefined && { events: input.events }),
|
|
1119
|
+
...(input.isActive !== undefined && { is_active: input.isActive }),
|
|
1120
|
+
...(input.headers !== undefined && { headers: input.headers }),
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Delete a webhook
|
|
1125
|
+
*
|
|
1126
|
+
* @example
|
|
1127
|
+
* ```typescript
|
|
1128
|
+
* await tuteliq.deleteWebhook('webhook-123')
|
|
1129
|
+
* ```
|
|
1130
|
+
*/
|
|
1131
|
+
async deleteWebhook(id) {
|
|
1132
|
+
return this.requestWithRetry('DELETE', `/api/v1/webhooks/${id}`);
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Send a test payload to a webhook
|
|
1136
|
+
*
|
|
1137
|
+
* @example
|
|
1138
|
+
* ```typescript
|
|
1139
|
+
* const result = await tuteliq.testWebhook('webhook-123')
|
|
1140
|
+
* console.log('Success:', result.success)
|
|
1141
|
+
* console.log('Latency:', result.latency_ms, 'ms')
|
|
1142
|
+
* ```
|
|
1143
|
+
*/
|
|
1144
|
+
async testWebhook(id) {
|
|
1145
|
+
return this.requestWithRetry('POST', '/api/v1/webhooks/test', { webhook_id: id });
|
|
1146
|
+
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Regenerate a webhook's signing secret
|
|
1149
|
+
*
|
|
1150
|
+
* The old secret is immediately invalidated.
|
|
1151
|
+
*
|
|
1152
|
+
* @example
|
|
1153
|
+
* ```typescript
|
|
1154
|
+
* const { secret } = await tuteliq.regenerateWebhookSecret('webhook-123')
|
|
1155
|
+
* // Update your verification logic with the new secret
|
|
1156
|
+
* ```
|
|
1157
|
+
*/
|
|
1158
|
+
async regenerateWebhookSecret(id) {
|
|
1159
|
+
return this.requestWithRetry('POST', `/api/v1/webhooks/${id}/regenerate-secret`);
|
|
1160
|
+
}
|
|
1161
|
+
// =========================================================================
|
|
1162
|
+
// Pricing Methods
|
|
1163
|
+
// =========================================================================
|
|
1164
|
+
/**
|
|
1165
|
+
* Get public pricing plans (no authentication required)
|
|
1166
|
+
*
|
|
1167
|
+
* @example
|
|
1168
|
+
* ```typescript
|
|
1169
|
+
* const { plans } = await tuteliq.getPricing()
|
|
1170
|
+
* plans.forEach(p => console.log(p.name, p.price))
|
|
1171
|
+
* ```
|
|
1172
|
+
*/
|
|
1173
|
+
async getPricing() {
|
|
1174
|
+
return this.requestWithRetry('GET', '/api/v1/pricing');
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Get detailed pricing plans with monthly/yearly prices
|
|
1178
|
+
*
|
|
1179
|
+
* @example
|
|
1180
|
+
* ```typescript
|
|
1181
|
+
* const { plans } = await tuteliq.getPricingDetails()
|
|
1182
|
+
* plans.forEach(p => console.log(p.name, p.price_monthly, p.api_calls_per_month))
|
|
1183
|
+
* ```
|
|
1184
|
+
*/
|
|
1185
|
+
async getPricingDetails() {
|
|
1186
|
+
return this.requestWithRetry('GET', '/api/v1/pricing/details');
|
|
1187
|
+
}
|
|
1188
|
+
// =========================================================================
|
|
1189
|
+
// Additional Usage Methods
|
|
1190
|
+
// =========================================================================
|
|
1191
|
+
/**
|
|
1192
|
+
* Get usage history for the past N days
|
|
1193
|
+
*
|
|
1194
|
+
* @param days - Number of days (1-30, defaults to 7)
|
|
1195
|
+
*
|
|
1196
|
+
* @example
|
|
1197
|
+
* ```typescript
|
|
1198
|
+
* const { days } = await tuteliq.getUsageHistory(14)
|
|
1199
|
+
* days.forEach(d => console.log(d.date, d.total_requests))
|
|
1200
|
+
* ```
|
|
1201
|
+
*/
|
|
1202
|
+
async getUsageHistory(days) {
|
|
1203
|
+
const params = new URLSearchParams();
|
|
1204
|
+
if (days != null)
|
|
1205
|
+
params.set('days', String(days));
|
|
1206
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
1207
|
+
return this.requestWithRetry('GET', `/api/v1/usage/history${query}`);
|
|
1208
|
+
}
|
|
1209
|
+
/**
|
|
1210
|
+
* Get usage broken down by tool/endpoint
|
|
1211
|
+
*
|
|
1212
|
+
* @param date - Date in YYYY-MM-DD format (defaults to today)
|
|
1213
|
+
*
|
|
1214
|
+
* @example
|
|
1215
|
+
* ```typescript
|
|
1216
|
+
* const result = await tuteliq.getUsageByTool()
|
|
1217
|
+
* console.log('Tools:', result.tools)
|
|
1218
|
+
* console.log('Endpoints:', result.endpoints)
|
|
1219
|
+
* ```
|
|
1220
|
+
*/
|
|
1221
|
+
async getUsageByTool(date) {
|
|
1222
|
+
const params = new URLSearchParams();
|
|
1223
|
+
if (date)
|
|
1224
|
+
params.set('date', date);
|
|
1225
|
+
const query = params.toString() ? `?${params.toString()}` : '';
|
|
1226
|
+
return this.requestWithRetry('GET', `/api/v1/usage/by-tool${query}`);
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Get monthly usage, limits, and upgrade recommendations
|
|
1230
|
+
*
|
|
1231
|
+
* @example
|
|
1232
|
+
* ```typescript
|
|
1233
|
+
* const monthly = await tuteliq.getUsageMonthly()
|
|
1234
|
+
* console.log('Used:', monthly.usage.used, '/', monthly.usage.limit)
|
|
1235
|
+
* console.log('Days left:', monthly.billing.days_remaining)
|
|
1236
|
+
*
|
|
1237
|
+
* if (monthly.recommendations?.should_upgrade) {
|
|
1238
|
+
* console.log('Consider upgrading to', monthly.recommendations.suggested_tier)
|
|
1239
|
+
* }
|
|
1240
|
+
* ```
|
|
1241
|
+
*/
|
|
1242
|
+
async getUsageMonthly() {
|
|
1243
|
+
return this.requestWithRetry('GET', '/api/v1/usage/monthly');
|
|
1244
|
+
}
|
|
1245
|
+
// =========================================================================
|
|
1246
|
+
// Voice Streaming
|
|
1247
|
+
// =========================================================================
|
|
1248
|
+
/**
|
|
1249
|
+
* Open a real-time voice streaming session over WebSocket.
|
|
1250
|
+
*
|
|
1251
|
+
* Requires the `ws` package as an optional peer dependency:
|
|
1252
|
+
* ```bash
|
|
1253
|
+
* npm install ws
|
|
1254
|
+
* ```
|
|
1255
|
+
*
|
|
1256
|
+
* @example
|
|
1257
|
+
* ```typescript
|
|
1258
|
+
* const session = client.voiceStream(
|
|
1259
|
+
* { intervalSeconds: 10, analysisTypes: ['bullying', 'unsafe'] },
|
|
1260
|
+
* {
|
|
1261
|
+
* onTranscription: (e) => console.log('Transcript:', e.text),
|
|
1262
|
+
* onAlert: (e) => console.log('Alert:', e.category, e.severity),
|
|
1263
|
+
* }
|
|
1264
|
+
* );
|
|
1265
|
+
*
|
|
1266
|
+
* // Send audio chunks as they arrive
|
|
1267
|
+
* session.sendAudio(audioBuffer);
|
|
1268
|
+
*
|
|
1269
|
+
* // End session and get summary
|
|
1270
|
+
* const summary = await session.end();
|
|
1271
|
+
* console.log('Risk:', summary.overall_risk);
|
|
1272
|
+
* ```
|
|
1273
|
+
*/
|
|
1274
|
+
voiceStream(config, handlers) {
|
|
1275
|
+
return createVoiceStream(this.apiKey, config, handlers);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
// Legacy export for backwards compatibility
|
|
1279
|
+
export { Tuteliq as TuteliqClient };
|
|
1280
|
+
//# sourceMappingURL=client.js.map
|