@voice-kit/core 0.1.1 → 0.1.3
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/dist/index.cjs +2137 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1466 -3
- package/dist/index.d.ts +1466 -3
- package/dist/index.js +2102 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -26
- package/dist/compliance.cjs +0 -343
- package/dist/compliance.cjs.map +0 -1
- package/dist/compliance.d.cts +0 -163
- package/dist/compliance.d.ts +0 -163
- package/dist/compliance.js +0 -335
- package/dist/compliance.js.map +0 -1
- package/dist/errors.cjs +0 -284
- package/dist/errors.cjs.map +0 -1
- package/dist/errors.d.cts +0 -175
- package/dist/errors.d.ts +0 -175
- package/dist/errors.js +0 -262
- package/dist/errors.js.map +0 -1
- package/dist/index-CkTG6DOa.d.cts +0 -319
- package/dist/index-CkTG6DOa.d.ts +0 -319
- package/dist/memory.cjs +0 -121
- package/dist/memory.cjs.map +0 -1
- package/dist/memory.d.cts +0 -29
- package/dist/memory.d.ts +0 -29
- package/dist/memory.js +0 -115
- package/dist/memory.js.map +0 -1
- package/dist/observability.cjs +0 -229
- package/dist/observability.cjs.map +0 -1
- package/dist/observability.d.cts +0 -122
- package/dist/observability.d.ts +0 -122
- package/dist/observability.js +0 -222
- package/dist/observability.js.map +0 -1
- package/dist/stt.cjs +0 -828
- package/dist/stt.cjs.map +0 -1
- package/dist/stt.d.cts +0 -308
- package/dist/stt.d.ts +0 -308
- package/dist/stt.js +0 -815
- package/dist/stt.js.map +0 -1
- package/dist/tts.cjs +0 -429
- package/dist/tts.cjs.map +0 -1
- package/dist/tts.d.cts +0 -151
- package/dist/tts.d.ts +0 -151
- package/dist/tts.js +0 -418
- package/dist/tts.js.map +0 -1
package/dist/compliance.cjs
DELETED
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var lruCache = require('lru-cache');
|
|
4
|
-
var promises = require('fs/promises');
|
|
5
|
-
var pino = require('pino');
|
|
6
|
-
var axios = require('axios');
|
|
7
|
-
var libphonenumberJs = require('libphonenumber-js');
|
|
8
|
-
|
|
9
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
10
|
-
|
|
11
|
-
var pino__default = /*#__PURE__*/_interopDefault(pino);
|
|
12
|
-
var axios__default = /*#__PURE__*/_interopDefault(axios);
|
|
13
|
-
|
|
14
|
-
// src/compliance/audit/index.ts
|
|
15
|
-
var logger = pino__default.default({ name: "@voice-kit/core:compliance:audit" });
|
|
16
|
-
var CallAuditLog = class {
|
|
17
|
-
/** LRU: up to 10,000 calls × 200 entries each = 2M entries max */
|
|
18
|
-
cache;
|
|
19
|
-
filePath;
|
|
20
|
-
constructor(options) {
|
|
21
|
-
this.filePath = options?.filePath;
|
|
22
|
-
this.cache = new lruCache.LRUCache({
|
|
23
|
-
max: options?.maxCalls ?? 1e4,
|
|
24
|
-
ttl: 4 * 60 * 60 * 1e3
|
|
25
|
-
// 4 hours
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Append an immutable audit entry for a call.
|
|
30
|
-
*
|
|
31
|
-
* @param callId The call identifier
|
|
32
|
-
* @param type Audit event type
|
|
33
|
-
* @param data Additional structured data
|
|
34
|
-
*/
|
|
35
|
-
append(callId, type, data = {}) {
|
|
36
|
-
const entry = Object.freeze({
|
|
37
|
-
id: `${callId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
38
|
-
callId,
|
|
39
|
-
type,
|
|
40
|
-
timestamp: /* @__PURE__ */ new Date(),
|
|
41
|
-
data: Object.freeze({ ...data })
|
|
42
|
-
});
|
|
43
|
-
const existing = this.cache.get(callId) ?? [];
|
|
44
|
-
this.cache.set(callId, [...existing, entry]);
|
|
45
|
-
logger.debug({ callId, type, entryId: entry.id }, "Audit entry appended");
|
|
46
|
-
if (this.filePath) {
|
|
47
|
-
this.writeToFile(entry).catch(
|
|
48
|
-
(err) => logger.error({ err, callId, type }, "Audit file write failed")
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
return entry;
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Get all audit entries for a call, in insertion order.
|
|
55
|
-
*
|
|
56
|
-
* @param callId The call identifier
|
|
57
|
-
*/
|
|
58
|
-
getEntries(callId) {
|
|
59
|
-
return Object.freeze(this.cache.get(callId) ?? []);
|
|
60
|
-
}
|
|
61
|
-
/**
|
|
62
|
-
* Get entries of a specific type for a call.
|
|
63
|
-
*/
|
|
64
|
-
getEntriesByType(callId, type) {
|
|
65
|
-
return this.getEntries(callId).filter((e) => e.type === type);
|
|
66
|
-
}
|
|
67
|
-
/** Write entry to JSONL file. @internal */
|
|
68
|
-
async writeToFile(entry) {
|
|
69
|
-
if (!this.filePath) return;
|
|
70
|
-
const line = JSON.stringify({
|
|
71
|
-
...entry,
|
|
72
|
-
timestamp: entry.timestamp.toISOString()
|
|
73
|
-
}) + "\n";
|
|
74
|
-
await promises.appendFile(this.filePath, line, "utf-8");
|
|
75
|
-
}
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
// src/errors/base.ts
|
|
79
|
-
var VoiceKitError = class extends Error {
|
|
80
|
-
code;
|
|
81
|
-
callId;
|
|
82
|
-
provider;
|
|
83
|
-
retryable;
|
|
84
|
-
severity;
|
|
85
|
-
cause;
|
|
86
|
-
constructor(params) {
|
|
87
|
-
super(params.message);
|
|
88
|
-
this.name = this.constructor.name;
|
|
89
|
-
this.code = params.code;
|
|
90
|
-
this.callId = params.callId;
|
|
91
|
-
this.provider = params.provider;
|
|
92
|
-
this.retryable = params.retryable ?? false;
|
|
93
|
-
this.severity = params.severity ?? "medium";
|
|
94
|
-
this.cause = params.cause;
|
|
95
|
-
Object.setPrototypeOf(this, new.target.prototype);
|
|
96
|
-
}
|
|
97
|
-
toJSON() {
|
|
98
|
-
return {
|
|
99
|
-
name: this.name,
|
|
100
|
-
code: this.code,
|
|
101
|
-
message: this.message,
|
|
102
|
-
callId: this.callId,
|
|
103
|
-
provider: this.provider,
|
|
104
|
-
retryable: this.retryable,
|
|
105
|
-
severity: this.severity
|
|
106
|
-
};
|
|
107
|
-
}
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// src/errors/compliance.ts
|
|
111
|
-
var ComplianceError = class extends VoiceKitError {
|
|
112
|
-
phoneNumber;
|
|
113
|
-
constructor(params) {
|
|
114
|
-
super({ ...params, provider: "trai" });
|
|
115
|
-
this.phoneNumber = params.phoneNumber;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
var DNCBlockedError = class extends ComplianceError {
|
|
119
|
-
constructor(phoneNumber, callId) {
|
|
120
|
-
super({
|
|
121
|
-
code: "COMPLIANCE_DNC_BLOCKED",
|
|
122
|
-
message: `Number ${phoneNumber} is on DNC registry \u2014 call blocked`,
|
|
123
|
-
callId,
|
|
124
|
-
phoneNumber,
|
|
125
|
-
retryable: false,
|
|
126
|
-
severity: "low"
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
var CallingHoursError = class extends ComplianceError {
|
|
131
|
-
constructor(phoneNumber, currentTime, callId) {
|
|
132
|
-
super({
|
|
133
|
-
code: "COMPLIANCE_OUTSIDE_CALLING_HOURS",
|
|
134
|
-
message: `Call to ${phoneNumber} blocked \u2014 outside TRAI calling hours (current: ${currentTime} IST)`,
|
|
135
|
-
callId,
|
|
136
|
-
phoneNumber,
|
|
137
|
-
retryable: false,
|
|
138
|
-
severity: "low"
|
|
139
|
-
});
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
var logger2 = pino__default.default({ name: "@voice-kit/core:compliance:trai" });
|
|
143
|
-
var TRAI_DND_API_MOCK = "https://api.trai.gov.in/dnd/check";
|
|
144
|
-
var DEFAULTS = {
|
|
145
|
-
disabled: false,
|
|
146
|
-
timezone: "Asia/Kolkata",
|
|
147
|
-
callingHoursStart: 9,
|
|
148
|
-
callingHoursEnd: 21,
|
|
149
|
-
dncApiEndpoint: TRAI_DND_API_MOCK
|
|
150
|
-
};
|
|
151
|
-
var DNC_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
152
|
-
var CONSENT_VALIDITY_MS = 180 * 24 * 60 * 60 * 1e3;
|
|
153
|
-
var TRAICompliance = class {
|
|
154
|
-
config;
|
|
155
|
-
http;
|
|
156
|
-
/** DNC check results cached for 24 hours per number. */
|
|
157
|
-
dncCache;
|
|
158
|
-
/** Consent records cached for 180 days. */
|
|
159
|
-
consentCache;
|
|
160
|
-
constructor(config) {
|
|
161
|
-
this.config = { ...DEFAULTS, ...config };
|
|
162
|
-
this.dncCache = new lruCache.LRUCache({
|
|
163
|
-
max: 1e5,
|
|
164
|
-
ttl: DNC_CACHE_TTL_MS
|
|
165
|
-
});
|
|
166
|
-
this.consentCache = new lruCache.LRUCache({
|
|
167
|
-
max: 5e4,
|
|
168
|
-
ttl: CONSENT_VALIDITY_MS
|
|
169
|
-
});
|
|
170
|
-
this.http = axios__default.default.create({
|
|
171
|
-
baseURL: this.config.dncApiEndpoint,
|
|
172
|
-
timeout: 5e3,
|
|
173
|
-
headers: { "Content-Type": "application/json" }
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
/**
|
|
177
|
-
* Check whether a call is permitted under TRAI rules.
|
|
178
|
-
* Checks: valid E.164, DNC registry, calling hours.
|
|
179
|
-
*
|
|
180
|
-
* @param params Call permission check parameters
|
|
181
|
-
* @throws DNCBlockedError if number is on DNC registry
|
|
182
|
-
* @throws CallingHoursError if outside allowed calling hours
|
|
183
|
-
* @throws ComplianceError if phone number is invalid
|
|
184
|
-
*
|
|
185
|
-
* @example
|
|
186
|
-
* ```ts
|
|
187
|
-
* const result = await trai.checkCallPermission({
|
|
188
|
-
* to: '+919876543210',
|
|
189
|
-
* purpose: 'TRANSACTIONAL',
|
|
190
|
-
* })
|
|
191
|
-
* if (!result.allowed) console.log(result.reason)
|
|
192
|
-
* ```
|
|
193
|
-
*/
|
|
194
|
-
async checkCallPermission(params) {
|
|
195
|
-
if (this.config.disabled) {
|
|
196
|
-
return { allowed: true, fromCache: false };
|
|
197
|
-
}
|
|
198
|
-
if (!libphonenumberJs.isValidPhoneNumber(params.to)) {
|
|
199
|
-
throw new ComplianceError({
|
|
200
|
-
code: "COMPLIANCE_INVALID_NUMBER",
|
|
201
|
-
message: `Invalid phone number: ${params.to}`,
|
|
202
|
-
phoneNumber: params.to,
|
|
203
|
-
retryable: false,
|
|
204
|
-
severity: "low"
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
|
-
const parsed = libphonenumberJs.parsePhoneNumberFromString(params.to);
|
|
208
|
-
const isIndianNumber = parsed?.countryCallingCode === "91";
|
|
209
|
-
if (!isIndianNumber) {
|
|
210
|
-
return { allowed: true, fromCache: false };
|
|
211
|
-
}
|
|
212
|
-
const scheduledAt = params.scheduledAt ?? /* @__PURE__ */ new Date();
|
|
213
|
-
if (!this.isWithinCallingHours(scheduledAt)) {
|
|
214
|
-
const timeStr = new Intl.DateTimeFormat("en-IN", {
|
|
215
|
-
timeZone: this.config.timezone,
|
|
216
|
-
hour: "2-digit",
|
|
217
|
-
minute: "2-digit",
|
|
218
|
-
hour12: false
|
|
219
|
-
}).format(scheduledAt);
|
|
220
|
-
throw new CallingHoursError(params.to, timeStr);
|
|
221
|
-
}
|
|
222
|
-
if (params.purpose === "EMERGENCY") {
|
|
223
|
-
return { allowed: true, fromCache: false };
|
|
224
|
-
}
|
|
225
|
-
const cacheKey = `${params.to}:${params.purpose}`;
|
|
226
|
-
const cached = this.dncCache.get(cacheKey);
|
|
227
|
-
if (cached) {
|
|
228
|
-
logger2.debug({ to: params.to, purpose: params.purpose, allowed: cached.allowed }, "DNC cache hit");
|
|
229
|
-
return { ...cached, fromCache: true };
|
|
230
|
-
}
|
|
231
|
-
const result = await this.fetchDNCStatus(params);
|
|
232
|
-
this.dncCache.set(cacheKey, result);
|
|
233
|
-
if (!result.allowed) {
|
|
234
|
-
throw new DNCBlockedError(params.to);
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
/**
|
|
239
|
-
* Check if the current time (or a given time) is within TRAI calling hours.
|
|
240
|
-
* Allowed: 9:00 AM – 9:00 PM IST.
|
|
241
|
-
* Uses Intl.DateTimeFormat only — no date-fns or dayjs dependency.
|
|
242
|
-
*
|
|
243
|
-
* @param at Time to check. Defaults to now.
|
|
244
|
-
* @param timezone IANA timezone. Defaults to 'Asia/Kolkata'.
|
|
245
|
-
*
|
|
246
|
-
* @example
|
|
247
|
-
* ```ts
|
|
248
|
-
* trai.isWithinCallingHours() // Check now
|
|
249
|
-
* trai.isWithinCallingHours(new Date()) // Explicit time
|
|
250
|
-
* ```
|
|
251
|
-
*/
|
|
252
|
-
isWithinCallingHours(at, timezone) {
|
|
253
|
-
const tz = timezone ?? this.config.timezone;
|
|
254
|
-
const date = at ?? /* @__PURE__ */ new Date();
|
|
255
|
-
const parts = new Intl.DateTimeFormat("en-IN", {
|
|
256
|
-
timeZone: tz,
|
|
257
|
-
hour: "numeric",
|
|
258
|
-
hour12: false
|
|
259
|
-
}).formatToParts(date);
|
|
260
|
-
const hourPart = parts.find((p) => p.type === "hour");
|
|
261
|
-
const hour = parseInt(hourPart?.value ?? "0", 10);
|
|
262
|
-
return hour >= this.config.callingHoursStart && hour < this.config.callingHoursEnd;
|
|
263
|
-
}
|
|
264
|
-
/**
|
|
265
|
-
* Record explicit consent from a user for future calls.
|
|
266
|
-
* Consent is valid for 180 days per TRAI guidelines.
|
|
267
|
-
*
|
|
268
|
-
* @param params Consent record details
|
|
269
|
-
*
|
|
270
|
-
* @example
|
|
271
|
-
* ```ts
|
|
272
|
-
* await trai.recordConsent({
|
|
273
|
-
* phoneNumber: '+919876543210',
|
|
274
|
-
* consentedAt: new Date(),
|
|
275
|
-
* channel: 'ivr',
|
|
276
|
-
* purpose: 'PROMOTIONAL',
|
|
277
|
-
* })
|
|
278
|
-
* ```
|
|
279
|
-
*/
|
|
280
|
-
async recordConsent(params) {
|
|
281
|
-
const normalized = libphonenumberJs.parsePhoneNumberFromString(params.phoneNumber)?.format("E.164");
|
|
282
|
-
this.consentCache.set(normalized, params);
|
|
283
|
-
logger2.info(
|
|
284
|
-
{ phoneNumber: normalized, purpose: params.purpose, channel: params.channel },
|
|
285
|
-
"Consent recorded"
|
|
286
|
-
);
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* Check if a number has valid (non-expired) consent on record.
|
|
290
|
-
*
|
|
291
|
-
* @param phoneNumber E.164 phone number
|
|
292
|
-
* @returns True if valid consent exists
|
|
293
|
-
*/
|
|
294
|
-
async hasValidConsent(phoneNumber) {
|
|
295
|
-
let normalized;
|
|
296
|
-
try {
|
|
297
|
-
normalized = libphonenumberJs.parsePhoneNumberFromString(phoneNumber)?.format("E.164");
|
|
298
|
-
} catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
const record = this.consentCache.get(normalized);
|
|
302
|
-
if (!record) return false;
|
|
303
|
-
const ageMs = Date.now() - record.consentedAt.getTime();
|
|
304
|
-
return ageMs < CONSENT_VALIDITY_MS;
|
|
305
|
-
}
|
|
306
|
-
/**
|
|
307
|
-
* Fetch DNC status from TRAI DND API.
|
|
308
|
-
* @internal
|
|
309
|
-
*/
|
|
310
|
-
async fetchDNCStatus(params) {
|
|
311
|
-
try {
|
|
312
|
-
logger2.debug({ to: params.to, purpose: params.purpose }, "Fetching DNC status from TRAI");
|
|
313
|
-
const response = await this.http.post("", {
|
|
314
|
-
phone: params.to,
|
|
315
|
-
type: params.purpose
|
|
316
|
-
});
|
|
317
|
-
const result = {
|
|
318
|
-
allowed: !response.data.registered,
|
|
319
|
-
reason: response.data.registered ? `Number is registered on DNC for category: ${response.data.category ?? "ALL"}` : void 0,
|
|
320
|
-
cachedAt: /* @__PURE__ */ new Date(),
|
|
321
|
-
fromCache: false
|
|
322
|
-
};
|
|
323
|
-
logger2.info({ to: params.to, allowed: result.allowed }, "DNC status fetched");
|
|
324
|
-
return result;
|
|
325
|
-
} catch (err) {
|
|
326
|
-
if (axios__default.default.isAxiosError(err) && err.response?.status === 404) {
|
|
327
|
-
return { allowed: true, cachedAt: /* @__PURE__ */ new Date(), fromCache: false };
|
|
328
|
-
}
|
|
329
|
-
logger2.error({ err, to: params.to }, "TRAI DNC API unavailable \u2014 failing open");
|
|
330
|
-
return {
|
|
331
|
-
allowed: true,
|
|
332
|
-
reason: "DNC check unavailable \u2014 failing open",
|
|
333
|
-
cachedAt: /* @__PURE__ */ new Date(),
|
|
334
|
-
fromCache: false
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
|
|
340
|
-
exports.CallAuditLog = CallAuditLog;
|
|
341
|
-
exports.TRAICompliance = TRAICompliance;
|
|
342
|
-
//# sourceMappingURL=compliance.cjs.map
|
|
343
|
-
//# sourceMappingURL=compliance.cjs.map
|
package/dist/compliance.cjs.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/compliance/audit/index.ts","../src/errors/base.ts","../src/errors/compliance.ts","../src/compliance/trai/index.ts"],"names":["pino","LRUCache","appendFile","logger","axios","isValidPhoneNumber","parsePhoneNumberFromString"],"mappings":"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","file":"compliance.cjs","sourcesContent":["/**\r\n * @voice-kit/core — Call audit log\r\n *\r\n * Immutable append-only audit log for compliance and debugging.\r\n * In-memory (LRU) + optional file sink. Once written, entries cannot be modified.\r\n */\r\n\r\nimport { LRUCache } from 'lru-cache'\r\nimport { appendFile } from 'node:fs/promises'\r\nimport pino from 'pino'\r\n\r\nconst logger = pino({ name: '@voice-kit/core:compliance:audit' })\r\n\r\nexport type AuditEventType =\r\n | 'call.started'\r\n | 'call.ended'\r\n | 'compliance.checked'\r\n | 'compliance.blocked'\r\n | 'consent.recorded'\r\n | 'consent.verified'\r\n | 'turn.started'\r\n | 'turn.ended'\r\n | 'interruption'\r\n | 'agent.handoff'\r\n | 'tool.called'\r\n | 'error'\r\n\r\nexport interface AuditEntry {\r\n readonly id: string\r\n readonly callId: string\r\n readonly type: AuditEventType\r\n readonly timestamp: Date\r\n readonly data: Readonly<Record<string, unknown>>\r\n}\r\n\r\n/**\r\n * Immutable append-only call audit log.\r\n *\r\n * Entries are written to LRU in-process memory and optionally to a JSONL file.\r\n * Once written, entries are frozen — no modification is possible.\r\n *\r\n * @example\r\n * ```ts\r\n * const audit = new CallAuditLog({ filePath: '/var/log/voice-kit/audit.jsonl' })\r\n * audit.append(callId, 'call.started', { from: '+91...', to: '+91...' })\r\n * const entries = audit.getEntries(callId)\r\n * ```\r\n */\r\nexport class CallAuditLog {\r\n /** LRU: up to 10,000 calls × 200 entries each = 2M entries max */\r\n private readonly cache: LRUCache<string, AuditEntry[]>\r\n private readonly filePath?: string\r\n\r\n constructor(options?: { filePath?: string; maxCalls?: number }) {\r\n this.filePath = options?.filePath\r\n this.cache = new LRUCache<string, AuditEntry[]>({\r\n max: options?.maxCalls ?? 10_000,\r\n ttl: 4 * 60 * 60 * 1_000, // 4 hours\r\n })\r\n }\r\n\r\n /**\r\n * Append an immutable audit entry for a call.\r\n *\r\n * @param callId The call identifier\r\n * @param type Audit event type\r\n * @param data Additional structured data\r\n */\r\n append(\r\n callId: string,\r\n type: AuditEventType,\r\n data: Record<string, unknown> = {}\r\n ): AuditEntry {\r\n const entry: AuditEntry = Object.freeze({\r\n id: `${callId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,\r\n callId,\r\n type,\r\n timestamp: new Date(),\r\n data: Object.freeze({ ...data }),\r\n })\r\n\r\n const existing = this.cache.get(callId) ?? []\r\n // Freeze the array slice to prevent mutation\r\n this.cache.set(callId, [...existing, entry])\r\n\r\n logger.debug({ callId, type, entryId: entry.id }, 'Audit entry appended')\r\n\r\n // Write to file sink asynchronously — never block the call\r\n if (this.filePath) {\r\n this.writeToFile(entry).catch((err) =>\r\n logger.error({ err, callId, type }, 'Audit file write failed')\r\n )\r\n }\r\n\r\n return entry\r\n }\r\n\r\n /**\r\n * Get all audit entries for a call, in insertion order.\r\n *\r\n * @param callId The call identifier\r\n */\r\n getEntries(callId: string): ReadonlyArray<AuditEntry> {\r\n return Object.freeze(this.cache.get(callId) ?? [])\r\n }\r\n\r\n /**\r\n * Get entries of a specific type for a call.\r\n */\r\n getEntriesByType(callId: string, type: AuditEventType): ReadonlyArray<AuditEntry> {\r\n return this.getEntries(callId).filter((e) => e.type === type)\r\n }\r\n\r\n /** Write entry to JSONL file. @internal */\r\n private async writeToFile(entry: AuditEntry): Promise<void> {\r\n if (!this.filePath) return\r\n const line = JSON.stringify({\r\n ...entry,\r\n timestamp: entry.timestamp.toISOString(),\r\n }) + '\\n'\r\n await appendFile(this.filePath, line, 'utf-8')\r\n }\r\n}","/**\r\n * @voice-kit/core — Typed error hierarchy\r\n *\r\n * All VoiceKit errors extend VoiceKitError. Never throw raw Error.\r\n * Every error carries: code, message, provider, callId, retryable, severity.\r\n */\r\n\r\nimport type { ErrorSeverity } from '../types'\r\n\r\n// ─── Base Error ───────────────────────────────────────────────────────────────\r\n\r\n/**\r\n * Base class for all VoiceKit errors. Provides structured context for\r\n * logging, alerting, and programmatic error handling.\r\n *\r\n * @example\r\n * ```ts\r\n * try {\r\n * await stt.transcribeBatch(audio)\r\n * } catch (err) {\r\n * if (err instanceof STTError) {\r\n * console.error(err.code, err.provider, err.retryable)\r\n * }\r\n * }\r\n * ```\r\n */\r\nexport class VoiceKitError extends Error {\r\n readonly code: string\r\n readonly callId?: string\r\n readonly provider?: string\r\n readonly retryable: boolean\r\n readonly severity: ErrorSeverity\r\n override readonly cause?: unknown\r\n\r\n constructor(params: {\r\n code: string\r\n message: string\r\n callId?: string\r\n provider?: string\r\n retryable?: boolean\r\n severity?: ErrorSeverity\r\n cause?: unknown\r\n }) {\r\n super(params.message)\r\n this.name = this.constructor.name\r\n this.code = params.code\r\n this.callId = params.callId\r\n this.provider = params.provider\r\n this.retryable = params.retryable ?? false\r\n this.severity = params.severity ?? 'medium'\r\n this.cause = params.cause\r\n\r\n // Maintains proper prototype chain for `instanceof` in transpiled code\r\n Object.setPrototypeOf(this, new.target.prototype)\r\n }\r\n\r\n toJSON() {\r\n return {\r\n name: this.name,\r\n code: this.code,\r\n message: this.message,\r\n callId: this.callId,\r\n provider: this.provider,\r\n retryable: this.retryable,\r\n severity: this.severity,\r\n }\r\n }\r\n}","\r\n// ─── Compliance Errors ────────────────────────────────────────────────────────\r\n\r\nimport type { ErrorSeverity } from \"../types\"\r\nimport { VoiceKitError } from \"./base\"\r\n\r\n/**\r\n * Errors from compliance checks (TRAI DNC, calling hours, consent).\r\n */\r\nexport class ComplianceError extends VoiceKitError {\r\n readonly phoneNumber?: string\r\n\r\n constructor(params: {\r\n code: string\r\n message: string\r\n callId?: string\r\n phoneNumber?: string\r\n retryable?: boolean\r\n severity?: ErrorSeverity\r\n cause?: unknown\r\n }) {\r\n super({ ...params, provider: 'trai' })\r\n this.phoneNumber = params.phoneNumber\r\n }\r\n}\r\n\r\nexport class DNCBlockedError extends ComplianceError {\r\n constructor(phoneNumber: string, callId?: string) {\r\n super({\r\n code: 'COMPLIANCE_DNC_BLOCKED',\r\n message: `Number ${phoneNumber} is on DNC registry — call blocked`,\r\n callId,\r\n phoneNumber,\r\n retryable: false,\r\n severity: 'low',\r\n })\r\n }\r\n}\r\n\r\nexport class CallingHoursError extends ComplianceError {\r\n constructor(phoneNumber: string, currentTime: string, callId?: string) {\r\n super({\r\n code: 'COMPLIANCE_OUTSIDE_CALLING_HOURS',\r\n message: `Call to ${phoneNumber} blocked — outside TRAI calling hours (current: ${currentTime} IST)`,\r\n callId,\r\n phoneNumber,\r\n retryable: false,\r\n severity: 'low',\r\n })\r\n }\r\n}\r\n\r\nexport class ConsentMissingError extends ComplianceError {\r\n constructor(phoneNumber: string, callId?: string) {\r\n super({\r\n code: 'COMPLIANCE_CONSENT_MISSING',\r\n message: `No valid consent on record for ${phoneNumber}`,\r\n callId,\r\n phoneNumber,\r\n retryable: false,\r\n severity: 'medium',\r\n })\r\n }\r\n}\r\n","/**\r\n * @voice-kit/core — TRAI Compliance\r\n *\r\n * TRAI (Telecom Regulatory Authority of India) compliance utilities:\r\n * - DNC (Do Not Call) registry check with 24h LRU cache\r\n * - Calling hours enforcement (9 AM – 9 PM IST)\r\n * - Consent tracking (180-day validity)\r\n *\r\n * Auto-enabled for +91 numbers. Opt-out, not opt-in.\r\n */\r\n\r\nimport axios, { type AxiosInstance } from 'axios'\r\nimport { LRUCache } from 'lru-cache'\r\nimport { parsePhoneNumberFromString, isValidPhoneNumber } from 'libphonenumber-js'\r\nimport {\r\n DNCBlockedError,\r\n CallingHoursError,\r\n ComplianceError,\r\n} from '../../errors'\r\nimport type {\r\n DNCCheckParams,\r\n DNCCheckResult,\r\n ConsentRecord,\r\n TRAIConfig,\r\n} from '../../types'\r\nimport pino from 'pino'\r\n\r\nconst logger = pino({ name: '@voice-kit/core:compliance:trai' })\r\n\r\n/** REPLACE WITH REAL TRAI DND API ENDPOINT in production. */\r\nconst TRAI_DND_API_MOCK = 'https://api.trai.gov.in/dnd/check' // REPLACE WITH REAL TRAI API ENDPOINT\r\n\r\nconst DEFAULTS: Required<TRAIConfig> = {\r\n disabled: false,\r\n timezone: 'Asia/Kolkata',\r\n callingHoursStart: 9,\r\n callingHoursEnd: 21,\r\n dncApiEndpoint: TRAI_DND_API_MOCK,\r\n}\r\n\r\n/** 24 hours in ms */\r\nconst DNC_CACHE_TTL_MS = 24 * 60 * 60 * 1_000\r\n\r\n/** Consent validity: 180 days in ms */\r\nconst CONSENT_VALIDITY_MS = 180 * 24 * 60 * 60 * 1_000\r\n\r\n/**\r\n * TRAI compliance engine.\r\n *\r\n * Enforces DNC registry, calling hours, and consent rules for Indian numbers.\r\n * Results are cached in LRU to minimize API round-trips.\r\n *\r\n * @example\r\n * ```ts\r\n * const trai = new TRAICompliance()\r\n *\r\n * const result = await trai.checkCallPermission({\r\n * to: '+919876543210',\r\n * purpose: 'TRANSACTIONAL',\r\n * })\r\n *\r\n * if (!result.allowed) throw new Error(result.reason)\r\n * ```\r\n */\r\nexport class TRAICompliance {\r\n private readonly config: Required<TRAIConfig>\r\n private readonly http: AxiosInstance\r\n\r\n /** DNC check results cached for 24 hours per number. */\r\n private readonly dncCache: LRUCache<string, DNCCheckResult>\r\n\r\n /** Consent records cached for 180 days. */\r\n private readonly consentCache: LRUCache<string, ConsentRecord>\r\n\r\n constructor(config?: TRAIConfig) {\r\n this.config = { ...DEFAULTS, ...config }\r\n\r\n this.dncCache = new LRUCache<string, DNCCheckResult>({\r\n max: 100_000,\r\n ttl: DNC_CACHE_TTL_MS,\r\n })\r\n\r\n this.consentCache = new LRUCache<string, ConsentRecord>({\r\n max: 50_000,\r\n ttl: CONSENT_VALIDITY_MS,\r\n })\r\n\r\n this.http = axios.create({\r\n baseURL: this.config.dncApiEndpoint,\r\n timeout: 5_000,\r\n headers: { 'Content-Type': 'application/json' },\r\n })\r\n }\r\n\r\n /**\r\n * Check whether a call is permitted under TRAI rules.\r\n * Checks: valid E.164, DNC registry, calling hours.\r\n *\r\n * @param params Call permission check parameters\r\n * @throws DNCBlockedError if number is on DNC registry\r\n * @throws CallingHoursError if outside allowed calling hours\r\n * @throws ComplianceError if phone number is invalid\r\n *\r\n * @example\r\n * ```ts\r\n * const result = await trai.checkCallPermission({\r\n * to: '+919876543210',\r\n * purpose: 'TRANSACTIONAL',\r\n * })\r\n * if (!result.allowed) console.log(result.reason)\r\n * ```\r\n */\r\n async checkCallPermission(params: DNCCheckParams): Promise<DNCCheckResult> {\r\n if (this.config.disabled) {\r\n return { allowed: true, fromCache: false }\r\n }\r\n\r\n // Validate E.164 format\r\n if (!isValidPhoneNumber(params.to)) {\r\n throw new ComplianceError({\r\n code: 'COMPLIANCE_INVALID_NUMBER',\r\n message: `Invalid phone number: ${params.to}`,\r\n phoneNumber: params.to,\r\n retryable: false,\r\n severity: 'low',\r\n })\r\n }\r\n\r\n // Only apply TRAI rules to Indian numbers\r\n const parsed = parsePhoneNumberFromString(params.to)\r\n const isIndianNumber = parsed?.countryCallingCode === '91'\r\n\r\n if (!isIndianNumber) {\r\n return { allowed: true, fromCache: false }\r\n }\r\n\r\n // Check calling hours first (synchronous, fast)\r\n const scheduledAt = params.scheduledAt ?? new Date()\r\n if (!this.isWithinCallingHours(scheduledAt)) {\r\n const timeStr = new Intl.DateTimeFormat('en-IN', {\r\n timeZone: this.config.timezone,\r\n hour: '2-digit',\r\n minute: '2-digit',\r\n hour12: false,\r\n }).format(scheduledAt)\r\n\r\n throw new CallingHoursError(params.to, timeStr)\r\n }\r\n\r\n // Exempt EMERGENCY purpose from DNC check\r\n if (params.purpose === 'EMERGENCY') {\r\n return { allowed: true, fromCache: false }\r\n }\r\n\r\n // Check DNC cache\r\n const cacheKey = `${params.to}:${params.purpose}`\r\n const cached = this.dncCache.get(cacheKey)\r\n if (cached) {\r\n logger.debug({ to: params.to, purpose: params.purpose, allowed: cached.allowed }, 'DNC cache hit')\r\n return { ...cached, fromCache: true }\r\n }\r\n\r\n // Fetch from TRAI DND API\r\n const result = await this.fetchDNCStatus(params)\r\n this.dncCache.set(cacheKey, result)\r\n\r\n if (!result.allowed) {\r\n throw new DNCBlockedError(params.to)\r\n }\r\n\r\n return result\r\n }\r\n\r\n /**\r\n * Check if the current time (or a given time) is within TRAI calling hours.\r\n * Allowed: 9:00 AM – 9:00 PM IST.\r\n * Uses Intl.DateTimeFormat only — no date-fns or dayjs dependency.\r\n *\r\n * @param at Time to check. Defaults to now.\r\n * @param timezone IANA timezone. Defaults to 'Asia/Kolkata'.\r\n *\r\n * @example\r\n * ```ts\r\n * trai.isWithinCallingHours() // Check now\r\n * trai.isWithinCallingHours(new Date()) // Explicit time\r\n * ```\r\n */\r\n isWithinCallingHours(at?: Date, timezone?: string): boolean {\r\n const tz = timezone ?? this.config.timezone\r\n const date = at ?? new Date()\r\n\r\n // Extract hour in target timezone using Intl\r\n const parts = new Intl.DateTimeFormat('en-IN', {\r\n timeZone: tz,\r\n hour: 'numeric',\r\n hour12: false,\r\n }).formatToParts(date)\r\n\r\n const hourPart = parts.find((p) => p.type === 'hour')\r\n const hour = parseInt(hourPart?.value ?? '0', 10)\r\n\r\n return hour >= this.config.callingHoursStart && hour < this.config.callingHoursEnd\r\n }\r\n\r\n /**\r\n * Record explicit consent from a user for future calls.\r\n * Consent is valid for 180 days per TRAI guidelines.\r\n *\r\n * @param params Consent record details\r\n *\r\n * @example\r\n * ```ts\r\n * await trai.recordConsent({\r\n * phoneNumber: '+919876543210',\r\n * consentedAt: new Date(),\r\n * channel: 'ivr',\r\n * purpose: 'PROMOTIONAL',\r\n * })\r\n * ```\r\n */\r\n async recordConsent(params: ConsentRecord): Promise<void> {\r\n const normalized = parsePhoneNumberFromString(params.phoneNumber)?.format('E.164')\r\n this.consentCache.set(normalized as string, params)\r\n\r\n logger.info(\r\n { phoneNumber: normalized, purpose: params.purpose, channel: params.channel },\r\n 'Consent recorded'\r\n )\r\n }\r\n\r\n /**\r\n * Check if a number has valid (non-expired) consent on record.\r\n *\r\n * @param phoneNumber E.164 phone number\r\n * @returns True if valid consent exists\r\n */\r\n async hasValidConsent(phoneNumber: string): Promise<boolean> {\r\n let normalized: string\r\n try {\r\n normalized = parsePhoneNumberFromString(phoneNumber)?.format('E.164') as string\r\n } catch {\r\n return false\r\n }\r\n\r\n const record = this.consentCache.get(normalized)\r\n if (!record) return false\r\n\r\n const ageMs = Date.now() - record.consentedAt.getTime()\r\n return ageMs < CONSENT_VALIDITY_MS\r\n }\r\n\r\n /**\r\n * Fetch DNC status from TRAI DND API.\r\n * @internal\r\n */\r\n private async fetchDNCStatus(params: DNCCheckParams): Promise<DNCCheckResult> {\r\n try {\r\n logger.debug({ to: params.to, purpose: params.purpose }, 'Fetching DNC status from TRAI')\r\n\r\n // REPLACE WITH REAL TRAI API ENDPOINT — current implementation is a mock\r\n const response = await this.http.post<{ registered: boolean; category?: string }>('', {\r\n phone: params.to,\r\n type: params.purpose,\r\n })\r\n\r\n const result: DNCCheckResult = {\r\n allowed: !response.data.registered,\r\n reason: response.data.registered\r\n ? `Number is registered on DNC for category: ${response.data.category ?? 'ALL'}`\r\n : undefined,\r\n cachedAt: new Date(),\r\n fromCache: false,\r\n }\r\n\r\n logger.info({ to: params.to, allowed: result.allowed }, 'DNC status fetched')\r\n return result\r\n } catch (err) {\r\n if (axios.isAxiosError(err) && err.response?.status === 404) {\r\n // Number not on DNC list\r\n return { allowed: true, cachedAt: new Date(), fromCache: false }\r\n }\r\n\r\n // On API failure, fail-open (allow call) but log the error\r\n logger.error({ err, to: params.to }, 'TRAI DNC API unavailable — failing open')\r\n return {\r\n allowed: true,\r\n reason: 'DNC check unavailable — failing open',\r\n cachedAt: new Date(),\r\n fromCache: false,\r\n }\r\n }\r\n }\r\n}"]}
|
package/dist/compliance.d.cts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { T as TRAIConfig, D as DNCCheckParams, b as DNCCheckResult, c as ConsentRecord } from './index-CkTG6DOa.cjs';
|
|
2
|
-
import 'ai';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @voice-kit/core — Call audit log
|
|
6
|
-
*
|
|
7
|
-
* Immutable append-only audit log for compliance and debugging.
|
|
8
|
-
* In-memory (LRU) + optional file sink. Once written, entries cannot be modified.
|
|
9
|
-
*/
|
|
10
|
-
type AuditEventType = 'call.started' | 'call.ended' | 'compliance.checked' | 'compliance.blocked' | 'consent.recorded' | 'consent.verified' | 'turn.started' | 'turn.ended' | 'interruption' | 'agent.handoff' | 'tool.called' | 'error';
|
|
11
|
-
interface AuditEntry {
|
|
12
|
-
readonly id: string;
|
|
13
|
-
readonly callId: string;
|
|
14
|
-
readonly type: AuditEventType;
|
|
15
|
-
readonly timestamp: Date;
|
|
16
|
-
readonly data: Readonly<Record<string, unknown>>;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Immutable append-only call audit log.
|
|
20
|
-
*
|
|
21
|
-
* Entries are written to LRU in-process memory and optionally to a JSONL file.
|
|
22
|
-
* Once written, entries are frozen — no modification is possible.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* const audit = new CallAuditLog({ filePath: '/var/log/voice-kit/audit.jsonl' })
|
|
27
|
-
* audit.append(callId, 'call.started', { from: '+91...', to: '+91...' })
|
|
28
|
-
* const entries = audit.getEntries(callId)
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
declare class CallAuditLog {
|
|
32
|
-
/** LRU: up to 10,000 calls × 200 entries each = 2M entries max */
|
|
33
|
-
private readonly cache;
|
|
34
|
-
private readonly filePath?;
|
|
35
|
-
constructor(options?: {
|
|
36
|
-
filePath?: string;
|
|
37
|
-
maxCalls?: number;
|
|
38
|
-
});
|
|
39
|
-
/**
|
|
40
|
-
* Append an immutable audit entry for a call.
|
|
41
|
-
*
|
|
42
|
-
* @param callId The call identifier
|
|
43
|
-
* @param type Audit event type
|
|
44
|
-
* @param data Additional structured data
|
|
45
|
-
*/
|
|
46
|
-
append(callId: string, type: AuditEventType, data?: Record<string, unknown>): AuditEntry;
|
|
47
|
-
/**
|
|
48
|
-
* Get all audit entries for a call, in insertion order.
|
|
49
|
-
*
|
|
50
|
-
* @param callId The call identifier
|
|
51
|
-
*/
|
|
52
|
-
getEntries(callId: string): ReadonlyArray<AuditEntry>;
|
|
53
|
-
/**
|
|
54
|
-
* Get entries of a specific type for a call.
|
|
55
|
-
*/
|
|
56
|
-
getEntriesByType(callId: string, type: AuditEventType): ReadonlyArray<AuditEntry>;
|
|
57
|
-
/** Write entry to JSONL file. @internal */
|
|
58
|
-
private writeToFile;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* @voice-kit/core — TRAI Compliance
|
|
63
|
-
*
|
|
64
|
-
* TRAI (Telecom Regulatory Authority of India) compliance utilities:
|
|
65
|
-
* - DNC (Do Not Call) registry check with 24h LRU cache
|
|
66
|
-
* - Calling hours enforcement (9 AM – 9 PM IST)
|
|
67
|
-
* - Consent tracking (180-day validity)
|
|
68
|
-
*
|
|
69
|
-
* Auto-enabled for +91 numbers. Opt-out, not opt-in.
|
|
70
|
-
*/
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* TRAI compliance engine.
|
|
74
|
-
*
|
|
75
|
-
* Enforces DNC registry, calling hours, and consent rules for Indian numbers.
|
|
76
|
-
* Results are cached in LRU to minimize API round-trips.
|
|
77
|
-
*
|
|
78
|
-
* @example
|
|
79
|
-
* ```ts
|
|
80
|
-
* const trai = new TRAICompliance()
|
|
81
|
-
*
|
|
82
|
-
* const result = await trai.checkCallPermission({
|
|
83
|
-
* to: '+919876543210',
|
|
84
|
-
* purpose: 'TRANSACTIONAL',
|
|
85
|
-
* })
|
|
86
|
-
*
|
|
87
|
-
* if (!result.allowed) throw new Error(result.reason)
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
declare class TRAICompliance {
|
|
91
|
-
private readonly config;
|
|
92
|
-
private readonly http;
|
|
93
|
-
/** DNC check results cached for 24 hours per number. */
|
|
94
|
-
private readonly dncCache;
|
|
95
|
-
/** Consent records cached for 180 days. */
|
|
96
|
-
private readonly consentCache;
|
|
97
|
-
constructor(config?: TRAIConfig);
|
|
98
|
-
/**
|
|
99
|
-
* Check whether a call is permitted under TRAI rules.
|
|
100
|
-
* Checks: valid E.164, DNC registry, calling hours.
|
|
101
|
-
*
|
|
102
|
-
* @param params Call permission check parameters
|
|
103
|
-
* @throws DNCBlockedError if number is on DNC registry
|
|
104
|
-
* @throws CallingHoursError if outside allowed calling hours
|
|
105
|
-
* @throws ComplianceError if phone number is invalid
|
|
106
|
-
*
|
|
107
|
-
* @example
|
|
108
|
-
* ```ts
|
|
109
|
-
* const result = await trai.checkCallPermission({
|
|
110
|
-
* to: '+919876543210',
|
|
111
|
-
* purpose: 'TRANSACTIONAL',
|
|
112
|
-
* })
|
|
113
|
-
* if (!result.allowed) console.log(result.reason)
|
|
114
|
-
* ```
|
|
115
|
-
*/
|
|
116
|
-
checkCallPermission(params: DNCCheckParams): Promise<DNCCheckResult>;
|
|
117
|
-
/**
|
|
118
|
-
* Check if the current time (or a given time) is within TRAI calling hours.
|
|
119
|
-
* Allowed: 9:00 AM – 9:00 PM IST.
|
|
120
|
-
* Uses Intl.DateTimeFormat only — no date-fns or dayjs dependency.
|
|
121
|
-
*
|
|
122
|
-
* @param at Time to check. Defaults to now.
|
|
123
|
-
* @param timezone IANA timezone. Defaults to 'Asia/Kolkata'.
|
|
124
|
-
*
|
|
125
|
-
* @example
|
|
126
|
-
* ```ts
|
|
127
|
-
* trai.isWithinCallingHours() // Check now
|
|
128
|
-
* trai.isWithinCallingHours(new Date()) // Explicit time
|
|
129
|
-
* ```
|
|
130
|
-
*/
|
|
131
|
-
isWithinCallingHours(at?: Date, timezone?: string): boolean;
|
|
132
|
-
/**
|
|
133
|
-
* Record explicit consent from a user for future calls.
|
|
134
|
-
* Consent is valid for 180 days per TRAI guidelines.
|
|
135
|
-
*
|
|
136
|
-
* @param params Consent record details
|
|
137
|
-
*
|
|
138
|
-
* @example
|
|
139
|
-
* ```ts
|
|
140
|
-
* await trai.recordConsent({
|
|
141
|
-
* phoneNumber: '+919876543210',
|
|
142
|
-
* consentedAt: new Date(),
|
|
143
|
-
* channel: 'ivr',
|
|
144
|
-
* purpose: 'PROMOTIONAL',
|
|
145
|
-
* })
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
recordConsent(params: ConsentRecord): Promise<void>;
|
|
149
|
-
/**
|
|
150
|
-
* Check if a number has valid (non-expired) consent on record.
|
|
151
|
-
*
|
|
152
|
-
* @param phoneNumber E.164 phone number
|
|
153
|
-
* @returns True if valid consent exists
|
|
154
|
-
*/
|
|
155
|
-
hasValidConsent(phoneNumber: string): Promise<boolean>;
|
|
156
|
-
/**
|
|
157
|
-
* Fetch DNC status from TRAI DND API.
|
|
158
|
-
* @internal
|
|
159
|
-
*/
|
|
160
|
-
private fetchDNCStatus;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
export { type AuditEntry, type AuditEventType, CallAuditLog, TRAICompliance };
|