@voice-kit/core 0.1.2 → 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.
Files changed (53) hide show
  1. package/dist/index.cjs +2137 -0
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +1466 -4
  4. package/dist/index.d.ts +1466 -4
  5. package/dist/index.js +2102 -1
  6. package/dist/index.js.map +1 -1
  7. package/package.json +1 -31
  8. package/dist/audio.cjs +0 -533
  9. package/dist/audio.cjs.map +0 -1
  10. package/dist/audio.d.cts +0 -260
  11. package/dist/audio.d.ts +0 -260
  12. package/dist/audio.js +0 -514
  13. package/dist/audio.js.map +0 -1
  14. package/dist/compliance.cjs +0 -343
  15. package/dist/compliance.cjs.map +0 -1
  16. package/dist/compliance.d.cts +0 -163
  17. package/dist/compliance.d.ts +0 -163
  18. package/dist/compliance.js +0 -335
  19. package/dist/compliance.js.map +0 -1
  20. package/dist/errors.cjs +0 -284
  21. package/dist/errors.cjs.map +0 -1
  22. package/dist/errors.d.cts +0 -100
  23. package/dist/errors.d.ts +0 -100
  24. package/dist/errors.js +0 -262
  25. package/dist/errors.js.map +0 -1
  26. package/dist/index-D3KfRXMP.d.cts +0 -319
  27. package/dist/index-D3KfRXMP.d.ts +0 -319
  28. package/dist/memory.cjs +0 -121
  29. package/dist/memory.cjs.map +0 -1
  30. package/dist/memory.d.cts +0 -29
  31. package/dist/memory.d.ts +0 -29
  32. package/dist/memory.js +0 -115
  33. package/dist/memory.js.map +0 -1
  34. package/dist/observability.cjs +0 -229
  35. package/dist/observability.cjs.map +0 -1
  36. package/dist/observability.d.cts +0 -122
  37. package/dist/observability.d.ts +0 -122
  38. package/dist/observability.js +0 -222
  39. package/dist/observability.js.map +0 -1
  40. package/dist/stt.cjs +0 -828
  41. package/dist/stt.cjs.map +0 -1
  42. package/dist/stt.d.cts +0 -308
  43. package/dist/stt.d.ts +0 -308
  44. package/dist/stt.js +0 -815
  45. package/dist/stt.js.map +0 -1
  46. package/dist/telephony.errors-BQYr6-vl.d.cts +0 -80
  47. package/dist/telephony.errors-C0-nScrF.d.ts +0 -80
  48. package/dist/tts.cjs +0 -429
  49. package/dist/tts.cjs.map +0 -1
  50. package/dist/tts.d.cts +0 -151
  51. package/dist/tts.d.ts +0 -151
  52. package/dist/tts.js +0 -418
  53. package/dist/tts.js.map +0 -1
@@ -1,163 +0,0 @@
1
- import { T as TRAIConfig, D as DNCCheckParams, b as DNCCheckResult, c as ConsentRecord } from './index-D3KfRXMP.js';
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 };
@@ -1,335 +0,0 @@
1
- import { LRUCache } from 'lru-cache';
2
- import { appendFile } from 'fs/promises';
3
- import pino from 'pino';
4
- import axios from 'axios';
5
- import { isValidPhoneNumber, parsePhoneNumberFromString } from 'libphonenumber-js';
6
-
7
- // src/compliance/audit/index.ts
8
- var logger = pino({ name: "@voice-kit/core:compliance:audit" });
9
- var CallAuditLog = class {
10
- /** LRU: up to 10,000 calls × 200 entries each = 2M entries max */
11
- cache;
12
- filePath;
13
- constructor(options) {
14
- this.filePath = options?.filePath;
15
- this.cache = new LRUCache({
16
- max: options?.maxCalls ?? 1e4,
17
- ttl: 4 * 60 * 60 * 1e3
18
- // 4 hours
19
- });
20
- }
21
- /**
22
- * Append an immutable audit entry for a call.
23
- *
24
- * @param callId The call identifier
25
- * @param type Audit event type
26
- * @param data Additional structured data
27
- */
28
- append(callId, type, data = {}) {
29
- const entry = Object.freeze({
30
- id: `${callId}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
31
- callId,
32
- type,
33
- timestamp: /* @__PURE__ */ new Date(),
34
- data: Object.freeze({ ...data })
35
- });
36
- const existing = this.cache.get(callId) ?? [];
37
- this.cache.set(callId, [...existing, entry]);
38
- logger.debug({ callId, type, entryId: entry.id }, "Audit entry appended");
39
- if (this.filePath) {
40
- this.writeToFile(entry).catch(
41
- (err) => logger.error({ err, callId, type }, "Audit file write failed")
42
- );
43
- }
44
- return entry;
45
- }
46
- /**
47
- * Get all audit entries for a call, in insertion order.
48
- *
49
- * @param callId The call identifier
50
- */
51
- getEntries(callId) {
52
- return Object.freeze(this.cache.get(callId) ?? []);
53
- }
54
- /**
55
- * Get entries of a specific type for a call.
56
- */
57
- getEntriesByType(callId, type) {
58
- return this.getEntries(callId).filter((e) => e.type === type);
59
- }
60
- /** Write entry to JSONL file. @internal */
61
- async writeToFile(entry) {
62
- if (!this.filePath) return;
63
- const line = JSON.stringify({
64
- ...entry,
65
- timestamp: entry.timestamp.toISOString()
66
- }) + "\n";
67
- await appendFile(this.filePath, line, "utf-8");
68
- }
69
- };
70
-
71
- // src/errors/base.ts
72
- var VoiceKitError = class extends Error {
73
- code;
74
- callId;
75
- provider;
76
- retryable;
77
- severity;
78
- cause;
79
- constructor(params) {
80
- super(params.message);
81
- this.name = this.constructor.name;
82
- this.code = params.code;
83
- this.callId = params.callId;
84
- this.provider = params.provider;
85
- this.retryable = params.retryable ?? false;
86
- this.severity = params.severity ?? "medium";
87
- this.cause = params.cause;
88
- Object.setPrototypeOf(this, new.target.prototype);
89
- }
90
- toJSON() {
91
- return {
92
- name: this.name,
93
- code: this.code,
94
- message: this.message,
95
- callId: this.callId,
96
- provider: this.provider,
97
- retryable: this.retryable,
98
- severity: this.severity
99
- };
100
- }
101
- };
102
-
103
- // src/errors/compliance.ts
104
- var ComplianceError = class extends VoiceKitError {
105
- phoneNumber;
106
- constructor(params) {
107
- super({ ...params, provider: "trai" });
108
- this.phoneNumber = params.phoneNumber;
109
- }
110
- };
111
- var DNCBlockedError = class extends ComplianceError {
112
- constructor(phoneNumber, callId) {
113
- super({
114
- code: "COMPLIANCE_DNC_BLOCKED",
115
- message: `Number ${phoneNumber} is on DNC registry \u2014 call blocked`,
116
- callId,
117
- phoneNumber,
118
- retryable: false,
119
- severity: "low"
120
- });
121
- }
122
- };
123
- var CallingHoursError = class extends ComplianceError {
124
- constructor(phoneNumber, currentTime, callId) {
125
- super({
126
- code: "COMPLIANCE_OUTSIDE_CALLING_HOURS",
127
- message: `Call to ${phoneNumber} blocked \u2014 outside TRAI calling hours (current: ${currentTime} IST)`,
128
- callId,
129
- phoneNumber,
130
- retryable: false,
131
- severity: "low"
132
- });
133
- }
134
- };
135
- var logger2 = pino({ name: "@voice-kit/core:compliance:trai" });
136
- var TRAI_DND_API_MOCK = "https://api.trai.gov.in/dnd/check";
137
- var DEFAULTS = {
138
- disabled: false,
139
- timezone: "Asia/Kolkata",
140
- callingHoursStart: 9,
141
- callingHoursEnd: 21,
142
- dncApiEndpoint: TRAI_DND_API_MOCK
143
- };
144
- var DNC_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
145
- var CONSENT_VALIDITY_MS = 180 * 24 * 60 * 60 * 1e3;
146
- var TRAICompliance = class {
147
- config;
148
- http;
149
- /** DNC check results cached for 24 hours per number. */
150
- dncCache;
151
- /** Consent records cached for 180 days. */
152
- consentCache;
153
- constructor(config) {
154
- this.config = { ...DEFAULTS, ...config };
155
- this.dncCache = new LRUCache({
156
- max: 1e5,
157
- ttl: DNC_CACHE_TTL_MS
158
- });
159
- this.consentCache = new LRUCache({
160
- max: 5e4,
161
- ttl: CONSENT_VALIDITY_MS
162
- });
163
- this.http = axios.create({
164
- baseURL: this.config.dncApiEndpoint,
165
- timeout: 5e3,
166
- headers: { "Content-Type": "application/json" }
167
- });
168
- }
169
- /**
170
- * Check whether a call is permitted under TRAI rules.
171
- * Checks: valid E.164, DNC registry, calling hours.
172
- *
173
- * @param params Call permission check parameters
174
- * @throws DNCBlockedError if number is on DNC registry
175
- * @throws CallingHoursError if outside allowed calling hours
176
- * @throws ComplianceError if phone number is invalid
177
- *
178
- * @example
179
- * ```ts
180
- * const result = await trai.checkCallPermission({
181
- * to: '+919876543210',
182
- * purpose: 'TRANSACTIONAL',
183
- * })
184
- * if (!result.allowed) console.log(result.reason)
185
- * ```
186
- */
187
- async checkCallPermission(params) {
188
- if (this.config.disabled) {
189
- return { allowed: true, fromCache: false };
190
- }
191
- if (!isValidPhoneNumber(params.to)) {
192
- throw new ComplianceError({
193
- code: "COMPLIANCE_INVALID_NUMBER",
194
- message: `Invalid phone number: ${params.to}`,
195
- phoneNumber: params.to,
196
- retryable: false,
197
- severity: "low"
198
- });
199
- }
200
- const parsed = parsePhoneNumberFromString(params.to);
201
- const isIndianNumber = parsed?.countryCallingCode === "91";
202
- if (!isIndianNumber) {
203
- return { allowed: true, fromCache: false };
204
- }
205
- const scheduledAt = params.scheduledAt ?? /* @__PURE__ */ new Date();
206
- if (!this.isWithinCallingHours(scheduledAt)) {
207
- const timeStr = new Intl.DateTimeFormat("en-IN", {
208
- timeZone: this.config.timezone,
209
- hour: "2-digit",
210
- minute: "2-digit",
211
- hour12: false
212
- }).format(scheduledAt);
213
- throw new CallingHoursError(params.to, timeStr);
214
- }
215
- if (params.purpose === "EMERGENCY") {
216
- return { allowed: true, fromCache: false };
217
- }
218
- const cacheKey = `${params.to}:${params.purpose}`;
219
- const cached = this.dncCache.get(cacheKey);
220
- if (cached) {
221
- logger2.debug({ to: params.to, purpose: params.purpose, allowed: cached.allowed }, "DNC cache hit");
222
- return { ...cached, fromCache: true };
223
- }
224
- const result = await this.fetchDNCStatus(params);
225
- this.dncCache.set(cacheKey, result);
226
- if (!result.allowed) {
227
- throw new DNCBlockedError(params.to);
228
- }
229
- return result;
230
- }
231
- /**
232
- * Check if the current time (or a given time) is within TRAI calling hours.
233
- * Allowed: 9:00 AM – 9:00 PM IST.
234
- * Uses Intl.DateTimeFormat only — no date-fns or dayjs dependency.
235
- *
236
- * @param at Time to check. Defaults to now.
237
- * @param timezone IANA timezone. Defaults to 'Asia/Kolkata'.
238
- *
239
- * @example
240
- * ```ts
241
- * trai.isWithinCallingHours() // Check now
242
- * trai.isWithinCallingHours(new Date()) // Explicit time
243
- * ```
244
- */
245
- isWithinCallingHours(at, timezone) {
246
- const tz = timezone ?? this.config.timezone;
247
- const date = at ?? /* @__PURE__ */ new Date();
248
- const parts = new Intl.DateTimeFormat("en-IN", {
249
- timeZone: tz,
250
- hour: "numeric",
251
- hour12: false
252
- }).formatToParts(date);
253
- const hourPart = parts.find((p) => p.type === "hour");
254
- const hour = parseInt(hourPart?.value ?? "0", 10);
255
- return hour >= this.config.callingHoursStart && hour < this.config.callingHoursEnd;
256
- }
257
- /**
258
- * Record explicit consent from a user for future calls.
259
- * Consent is valid for 180 days per TRAI guidelines.
260
- *
261
- * @param params Consent record details
262
- *
263
- * @example
264
- * ```ts
265
- * await trai.recordConsent({
266
- * phoneNumber: '+919876543210',
267
- * consentedAt: new Date(),
268
- * channel: 'ivr',
269
- * purpose: 'PROMOTIONAL',
270
- * })
271
- * ```
272
- */
273
- async recordConsent(params) {
274
- const normalized = parsePhoneNumberFromString(params.phoneNumber)?.format("E.164");
275
- this.consentCache.set(normalized, params);
276
- logger2.info(
277
- { phoneNumber: normalized, purpose: params.purpose, channel: params.channel },
278
- "Consent recorded"
279
- );
280
- }
281
- /**
282
- * Check if a number has valid (non-expired) consent on record.
283
- *
284
- * @param phoneNumber E.164 phone number
285
- * @returns True if valid consent exists
286
- */
287
- async hasValidConsent(phoneNumber) {
288
- let normalized;
289
- try {
290
- normalized = parsePhoneNumberFromString(phoneNumber)?.format("E.164");
291
- } catch {
292
- return false;
293
- }
294
- const record = this.consentCache.get(normalized);
295
- if (!record) return false;
296
- const ageMs = Date.now() - record.consentedAt.getTime();
297
- return ageMs < CONSENT_VALIDITY_MS;
298
- }
299
- /**
300
- * Fetch DNC status from TRAI DND API.
301
- * @internal
302
- */
303
- async fetchDNCStatus(params) {
304
- try {
305
- logger2.debug({ to: params.to, purpose: params.purpose }, "Fetching DNC status from TRAI");
306
- const response = await this.http.post("", {
307
- phone: params.to,
308
- type: params.purpose
309
- });
310
- const result = {
311
- allowed: !response.data.registered,
312
- reason: response.data.registered ? `Number is registered on DNC for category: ${response.data.category ?? "ALL"}` : void 0,
313
- cachedAt: /* @__PURE__ */ new Date(),
314
- fromCache: false
315
- };
316
- logger2.info({ to: params.to, allowed: result.allowed }, "DNC status fetched");
317
- return result;
318
- } catch (err) {
319
- if (axios.isAxiosError(err) && err.response?.status === 404) {
320
- return { allowed: true, cachedAt: /* @__PURE__ */ new Date(), fromCache: false };
321
- }
322
- logger2.error({ err, to: params.to }, "TRAI DNC API unavailable \u2014 failing open");
323
- return {
324
- allowed: true,
325
- reason: "DNC check unavailable \u2014 failing open",
326
- cachedAt: /* @__PURE__ */ new Date(),
327
- fromCache: false
328
- };
329
- }
330
- }
331
- };
332
-
333
- export { CallAuditLog, TRAICompliance };
334
- //# sourceMappingURL=compliance.js.map
335
- //# sourceMappingURL=compliance.js.map
@@ -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":["logger","pino","LRUCache"],"mappings":"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","file":"compliance.js","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}"]}