@absolutejs/voice 0.0.22-beta.270 → 0.0.22-beta.272

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.d.ts CHANGED
@@ -160,13 +160,13 @@ export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqlit
160
160
  export type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceOpsTaskAgeBucket, VoiceOpsTaskAnalyticsOptions, VoiceOpsTaskAnalyticsSummary, VoiceOpsTaskAssignmentRule, VoiceOpsTaskAssignmentRuleCondition, VoiceOpsTaskAssignmentRules, VoiceOpsTaskAssigneeAnalytics, VoiceOpsDispositionTaskPolicies, VoiceOpsSLABreachPolicy, VoiceIntegrationDeliveryStatus, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceIntegrationSinkDelivery, VoiceIntegrationEventType, VoiceIntegrationWebhookConfig, VoiceOpsTask, VoiceOpsTaskHistoryEntry, VoiceOpsTaskKind, VoiceOpsTaskPolicy, VoiceOpsTaskPriority, VoiceOpsTaskStatus, VoiceOpsTaskStore, VoiceOpsTaskSummary, VoiceOpsTaskWorkerAnalytics } from './ops';
161
161
  export { createTwilioMediaStreamBridge, createTwilioVoiceRoutes, createTwilioVoiceResponse, decodeTwilioMulawBase64, encodeTwilioMulawBase64, transcodePCMToTwilioOutboundPayload, transcodeTwilioInboundPayloadToPCM16 } from './telephony/twilio';
162
162
  export { evaluateVoiceTelephonyContract } from './telephony/contract';
163
- export { createTelnyxMediaStreamBridge, createTelnyxVoiceResponse, createTelnyxVoiceRoutes, verifyVoiceTelnyxWebhookSignature } from './telephony/telnyx';
164
- export { createMemoryVoicePlivoWebhookNonceStore, createPlivoMediaStreamBridge, createPlivoVoiceResponse, createPlivoVoiceRoutes, createVoicePlivoWebhookVerifier, signVoicePlivoWebhook, verifyVoicePlivoWebhookSignature } from './telephony/plivo';
163
+ export { createMemoryVoiceTelnyxWebhookEventStore, createTelnyxMediaStreamBridge, createTelnyxVoiceResponse, createTelnyxVoiceRoutes, createVoicePostgresTelnyxWebhookEventStore, createVoiceRedisTelnyxWebhookEventStore, createVoiceSQLiteTelnyxWebhookEventStore, createVoiceTelnyxWebhookVerifier, verifyVoiceTelnyxWebhookSignature } from './telephony/telnyx';
164
+ export { createMemoryVoicePlivoWebhookNonceStore, createPlivoMediaStreamBridge, createPlivoVoiceResponse, createPlivoVoiceRoutes, createVoicePostgresPlivoWebhookNonceStore, createVoicePlivoWebhookVerifier, createVoiceRedisPlivoWebhookNonceStore, createVoiceSQLitePlivoWebhookNonceStore, signVoicePlivoWebhook, verifyVoicePlivoWebhookSignature } from './telephony/plivo';
165
165
  export { createVoiceTelephonyCarrierMatrix, createVoiceTelephonyCarrierMatrixRoutes, renderVoiceTelephonyCarrierMatrixHTML } from './telephony/matrix';
166
166
  export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceSmokeCheck, TwilioVoiceSmokeOptions, TwilioVoiceSmokeReport, TwilioVoiceSetupOptions, TwilioVoiceSetupStatus, TwilioVoiceRoutesOptions } from './telephony/twilio';
167
167
  export type { VoiceTelephonyContractIssue, VoiceTelephonyContractOptions, VoiceTelephonyContractReport, VoiceTelephonyContractRequirement, VoiceTelephonyProvider, VoiceTelephonySetupStatus, VoiceTelephonySmokeCheck, VoiceTelephonySmokeReport } from './telephony/contract';
168
- export type { TelnyxInboundMessage, TelnyxMediaPayload, TelnyxMediaStreamBridge, TelnyxMediaStreamBridgeOptions, TelnyxMediaStreamSocket, TelnyxOutboundClearMessage, TelnyxOutboundMarkMessage, TelnyxOutboundMediaMessage, TelnyxOutboundMessage, TelnyxVoiceResponseOptions, TelnyxVoiceRoutesOptions, TelnyxVoiceSetupOptions, TelnyxVoiceSetupStatus, TelnyxVoiceSmokeCheck, TelnyxVoiceSmokeOptions, TelnyxVoiceSmokeReport } from './telephony/telnyx';
169
- export type { PlivoInboundMessage, PlivoMediaStreamBridge, PlivoMediaStreamBridgeOptions, PlivoMediaStreamSocket, PlivoOutboundCheckpointMessage, PlivoOutboundClearAudioMessage, PlivoOutboundMessage, PlivoOutboundPlayAudioMessage, PlivoVoiceResponseOptions, PlivoVoiceRoutesOptions, PlivoVoiceSetupOptions, PlivoVoiceSetupStatus, PlivoVoiceSmokeCheck, PlivoVoiceSmokeOptions, PlivoVoiceSmokeReport, VoicePlivoWebhookNonceStore, VoicePlivoWebhookVerifierOptions } from './telephony/plivo';
168
+ export type { TelnyxInboundMessage, TelnyxMediaPayload, TelnyxMediaStreamBridge, TelnyxMediaStreamBridgeOptions, TelnyxMediaStreamSocket, TelnyxOutboundClearMessage, TelnyxOutboundMarkMessage, TelnyxOutboundMediaMessage, TelnyxOutboundMessage, TelnyxVoiceResponseOptions, TelnyxVoiceRoutesOptions, TelnyxVoiceSetupOptions, TelnyxVoiceSetupStatus, TelnyxVoiceSmokeCheck, TelnyxVoiceSmokeOptions, TelnyxVoiceSmokeReport, VoicePostgresTelnyxWebhookEventStoreOptions, VoiceRedisTelnyxWebhookEventClient, VoiceRedisTelnyxWebhookEventStoreOptions, VoiceSQLiteTelnyxWebhookEventStoreOptions, VoiceTelnyxWebhookEventStore, VoiceTelnyxWebhookEventStoreOptions, VoiceTelnyxWebhookVerifierOptions } from './telephony/telnyx';
169
+ export type { PlivoInboundMessage, PlivoMediaStreamBridge, PlivoMediaStreamBridgeOptions, PlivoMediaStreamSocket, PlivoOutboundCheckpointMessage, PlivoOutboundClearAudioMessage, PlivoOutboundMessage, PlivoOutboundPlayAudioMessage, PlivoVoiceResponseOptions, PlivoVoiceRoutesOptions, PlivoVoiceSetupOptions, PlivoVoiceSetupStatus, PlivoVoiceSmokeCheck, PlivoVoiceSmokeOptions, PlivoVoiceSmokeReport, VoicePostgresPlivoWebhookNonceStoreOptions, VoicePlivoWebhookNonceStore, VoicePlivoWebhookNonceStoreOptions, VoicePlivoWebhookVerifierOptions, VoiceRedisPlivoWebhookNonceClient, VoiceRedisPlivoWebhookNonceStoreOptions, VoiceSQLitePlivoWebhookNonceStoreOptions } from './telephony/plivo';
170
170
  export type { VoiceTelephonyCarrierMatrix, VoiceTelephonyCarrierMatrixEntry, VoiceTelephonyCarrierMatrixInput, VoiceTelephonyCarrierMatrixOptions, VoiceTelephonyCarrierMatrixRoutesOptions, VoiceTelephonyCarrierMatrixStatus } from './telephony/matrix';
171
171
  export { shapeTelephonyAssistantText } from './telephony/response';
172
172
  export type { TelephonyResponseShapeMode, TelephonyResponseShapeOptions } from './telephony/response';
package/dist/index.js CHANGED
@@ -18109,6 +18109,7 @@ import { Elysia as Elysia34 } from "elysia";
18109
18109
 
18110
18110
  // src/telephony/plivo.ts
18111
18111
  import { Buffer as Buffer5 } from "buffer";
18112
+ import { Database } from "bun:sqlite";
18112
18113
  import { Elysia as Elysia30 } from "elysia";
18113
18114
 
18114
18115
  // src/telephony/contract.ts
@@ -19125,12 +19126,148 @@ var verifyVoicePlivoWebhookSignature = async (input) => {
19125
19126
  var createMemoryVoicePlivoWebhookNonceStore = () => {
19126
19127
  const nonces = new Set;
19127
19128
  return {
19129
+ claim: (nonce) => {
19130
+ if (nonces.has(nonce)) {
19131
+ return false;
19132
+ }
19133
+ nonces.add(nonce);
19134
+ return true;
19135
+ },
19128
19136
  has: (nonce) => nonces.has(nonce),
19129
19137
  set: (nonce) => {
19130
19138
  nonces.add(nonce);
19131
19139
  }
19132
19140
  };
19133
19141
  };
19142
+ var normalizePlivoStoreIdentifierSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
19143
+ var quotePlivoStoreIdentifier = (value) => `"${value.replace(/"/g, '""')}"`;
19144
+ var resolvePlivoNonceTableName = (input) => {
19145
+ if (input.tableName) {
19146
+ return normalizePlivoStoreIdentifierSegment(input.tableName);
19147
+ }
19148
+ return `${normalizePlivoStoreIdentifierSegment(input.tablePrefix ?? "voice")}_${normalizePlivoStoreIdentifierSegment(input.fallback)}`;
19149
+ };
19150
+ var getPlivoNonceExpiresAt = (ttlSeconds) => typeof ttlSeconds === "number" && ttlSeconds > 0 ? Date.now() + Math.ceil(ttlSeconds * 1000) : null;
19151
+ var createVoiceSQLitePlivoWebhookNonceStore = (options) => {
19152
+ const database = options.database ?? new Database(options.path ?? ":memory:", {
19153
+ create: true
19154
+ });
19155
+ const tableName = resolvePlivoNonceTableName({
19156
+ fallback: "plivo_webhook_nonces",
19157
+ tableName: options.tableName,
19158
+ tablePrefix: options.tablePrefix
19159
+ });
19160
+ database.exec(`CREATE TABLE IF NOT EXISTS "${tableName}" (
19161
+ nonce TEXT PRIMARY KEY,
19162
+ created_at INTEGER NOT NULL,
19163
+ expires_at INTEGER
19164
+ )`);
19165
+ const pruneExpired = database.query(`DELETE FROM "${tableName}" WHERE expires_at IS NOT NULL AND expires_at <= ?1`);
19166
+ const select = database.query(`SELECT nonce FROM "${tableName}" WHERE nonce = ?1 AND (expires_at IS NULL OR expires_at > ?2) LIMIT 1`);
19167
+ const insert = database.query(`INSERT OR IGNORE INTO "${tableName}" (nonce, created_at, expires_at) VALUES (?1, ?2, ?3)`);
19168
+ const upsert = database.query(`INSERT INTO "${tableName}" (nonce, created_at, expires_at) VALUES (?1, ?2, ?3)
19169
+ ON CONFLICT(nonce) DO UPDATE SET expires_at = excluded.expires_at`);
19170
+ return {
19171
+ claim: (nonce) => {
19172
+ const now = Date.now();
19173
+ pruneExpired.run(now);
19174
+ const result = insert.run(nonce, now, getPlivoNonceExpiresAt(options.ttlSeconds));
19175
+ return result.changes > 0;
19176
+ },
19177
+ has: (nonce) => Boolean(select.get(nonce, Date.now())),
19178
+ set: (nonce) => {
19179
+ upsert.run(nonce, Date.now(), getPlivoNonceExpiresAt(options.ttlSeconds));
19180
+ }
19181
+ };
19182
+ };
19183
+ var createVoicePlivoPostgresClient = async (options) => {
19184
+ if (options.sql) {
19185
+ return options.sql;
19186
+ }
19187
+ if (!options.connectionString) {
19188
+ throw new Error("createVoicePostgresPlivoWebhookNonceStore requires either options.sql or options.connectionString.");
19189
+ }
19190
+ const sql = new Bun.SQL(options.connectionString);
19191
+ return {
19192
+ unsafe: sql.unsafe.bind(sql)
19193
+ };
19194
+ };
19195
+ var resolvePlivoNonceQualifiedTableName = (options) => {
19196
+ const schema = normalizePlivoStoreIdentifierSegment(options.schemaName ?? "public");
19197
+ const table = resolvePlivoNonceTableName({
19198
+ fallback: "plivo_webhook_nonces",
19199
+ tableName: options.tableName,
19200
+ tablePrefix: options.tablePrefix
19201
+ });
19202
+ return `${quotePlivoStoreIdentifier(schema)}.${quotePlivoStoreIdentifier(table)}`;
19203
+ };
19204
+ var createVoicePostgresPlivoWebhookNonceStore = (options = {}) => {
19205
+ const qualifiedTableName = resolvePlivoNonceQualifiedTableName(options);
19206
+ const schemaMatch = qualifiedTableName.match(/^"([^"]+)"\./);
19207
+ const client = createVoicePlivoPostgresClient(options);
19208
+ const initialized = (async () => {
19209
+ const sql = await client;
19210
+ if (schemaMatch?.[1]) {
19211
+ await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quotePlivoStoreIdentifier(schemaMatch[1])}`);
19212
+ }
19213
+ await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${qualifiedTableName} (
19214
+ nonce TEXT PRIMARY KEY,
19215
+ created_at BIGINT NOT NULL,
19216
+ expires_at BIGINT
19217
+ )`);
19218
+ })();
19219
+ const pruneExpired = async () => {
19220
+ await initialized;
19221
+ const sql = await client;
19222
+ await sql.unsafe(`DELETE FROM ${qualifiedTableName} WHERE expires_at IS NOT NULL AND expires_at <= $1`, [Date.now()]);
19223
+ };
19224
+ return {
19225
+ claim: async (nonce) => {
19226
+ await pruneExpired();
19227
+ const sql = await client;
19228
+ const rows = await sql.unsafe(`INSERT INTO ${qualifiedTableName} (nonce, created_at, expires_at)
19229
+ VALUES ($1, $2, $3)
19230
+ ON CONFLICT (nonce) DO NOTHING
19231
+ RETURNING nonce`, [nonce, Date.now(), getPlivoNonceExpiresAt(options.ttlSeconds)]);
19232
+ return rows.length > 0;
19233
+ },
19234
+ has: async (nonce) => {
19235
+ await initialized;
19236
+ const sql = await client;
19237
+ const rows = await sql.unsafe(`SELECT nonce FROM ${qualifiedTableName}
19238
+ WHERE nonce = $1 AND (expires_at IS NULL OR expires_at > $2)
19239
+ LIMIT 1`, [nonce, Date.now()]);
19240
+ return rows.length > 0;
19241
+ },
19242
+ set: async (nonce) => {
19243
+ await initialized;
19244
+ const sql = await client;
19245
+ await sql.unsafe(`INSERT INTO ${qualifiedTableName} (nonce, created_at, expires_at)
19246
+ VALUES ($1, $2, $3)
19247
+ ON CONFLICT (nonce) DO UPDATE SET expires_at = EXCLUDED.expires_at`, [nonce, Date.now(), getPlivoNonceExpiresAt(options.ttlSeconds)]);
19248
+ }
19249
+ };
19250
+ };
19251
+ var getPlivoRedisNonceKey = (keyPrefix, nonce) => `${keyPrefix}:${nonce}`;
19252
+ var createVoiceRedisPlivoWebhookNonceStore = (options = {}) => {
19253
+ const client = options.client ?? new Bun.RedisClient(options.url);
19254
+ const keyPrefix = options.keyPrefix?.trim() || "voice:plivo-webhook-nonce";
19255
+ const ttlSeconds = options.ttlSeconds;
19256
+ const setNonce = async (nonce, nx) => {
19257
+ const key = getPlivoRedisNonceKey(keyPrefix, nonce);
19258
+ if (typeof ttlSeconds === "number" && ttlSeconds > 0) {
19259
+ return client.set(key, "1", "EX", String(Math.ceil(ttlSeconds)), ...nx ? ["NX"] : []);
19260
+ }
19261
+ return client.set(key, "1", ...nx ? ["NX"] : []);
19262
+ };
19263
+ return {
19264
+ claim: async (nonce) => await setNonce(nonce, true) === "OK",
19265
+ has: async (nonce) => Boolean(await client.exists(getPlivoRedisNonceKey(keyPrefix, nonce))),
19266
+ set: async (nonce) => {
19267
+ await setNonce(nonce, false);
19268
+ }
19269
+ };
19270
+ };
19134
19271
  var createVoicePlivoWebhookVerifier = (options) => async (input) => {
19135
19272
  const verificationUrl = options.verificationUrl;
19136
19273
  const verification = await verifyVoicePlivoWebhookSignature({
@@ -19150,7 +19287,16 @@ var createVoicePlivoWebhookVerifier = (options) => async (input) => {
19150
19287
  return verification;
19151
19288
  }
19152
19289
  const nonce = input.headers.get("x-plivo-signature-v3-nonce");
19153
- if (!nonce || await nonceStore.has(nonce)) {
19290
+ if (!nonce) {
19291
+ return { ok: false, reason: "invalid-signature" };
19292
+ }
19293
+ if (nonceStore.claim) {
19294
+ if (!await nonceStore.claim(nonce)) {
19295
+ return { ok: false, reason: "invalid-signature" };
19296
+ }
19297
+ return verification;
19298
+ }
19299
+ if (await nonceStore.has(nonce)) {
19154
19300
  return { ok: false, reason: "invalid-signature" };
19155
19301
  }
19156
19302
  await nonceStore.set(nonce);
@@ -19403,6 +19549,7 @@ var createPlivoVoiceRoutes = (options = {}) => {
19403
19549
 
19404
19550
  // src/telephony/telnyx.ts
19405
19551
  import { Buffer as Buffer6 } from "buffer";
19552
+ import { Database as Database2 } from "bun:sqlite";
19406
19553
  import { Elysia as Elysia31 } from "elysia";
19407
19554
  var escapeXml4 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
19408
19555
  var escapeHtml30 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&#39;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -19575,6 +19722,198 @@ var verifyVoiceTelnyxWebhookSignature = async (input) => {
19575
19722
  return { ok: false, reason: "invalid-signature" };
19576
19723
  }
19577
19724
  };
19725
+ var createMemoryVoiceTelnyxWebhookEventStore = () => {
19726
+ const eventIds = new Set;
19727
+ return {
19728
+ claim: (eventId) => {
19729
+ if (eventIds.has(eventId)) {
19730
+ return false;
19731
+ }
19732
+ eventIds.add(eventId);
19733
+ return true;
19734
+ },
19735
+ has: (eventId) => eventIds.has(eventId),
19736
+ set: (eventId) => {
19737
+ eventIds.add(eventId);
19738
+ }
19739
+ };
19740
+ };
19741
+ var normalizeTelnyxStoreIdentifierSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
19742
+ var quoteTelnyxStoreIdentifier = (value) => `"${value.replace(/"/g, '""')}"`;
19743
+ var resolveTelnyxEventTableName = (input) => {
19744
+ if (input.tableName) {
19745
+ return normalizeTelnyxStoreIdentifierSegment(input.tableName);
19746
+ }
19747
+ return `${normalizeTelnyxStoreIdentifierSegment(input.tablePrefix ?? "voice")}_${normalizeTelnyxStoreIdentifierSegment(input.fallback)}`;
19748
+ };
19749
+ var getTelnyxEventExpiresAt = (ttlSeconds) => typeof ttlSeconds === "number" && ttlSeconds > 0 ? Date.now() + Math.ceil(ttlSeconds * 1000) : null;
19750
+ var createVoiceSQLiteTelnyxWebhookEventStore = (options) => {
19751
+ const database = options.database ?? new Database2(options.path ?? ":memory:", {
19752
+ create: true
19753
+ });
19754
+ const tableName = resolveTelnyxEventTableName({
19755
+ fallback: "telnyx_webhook_events",
19756
+ tableName: options.tableName,
19757
+ tablePrefix: options.tablePrefix
19758
+ });
19759
+ database.exec(`CREATE TABLE IF NOT EXISTS "${tableName}" (
19760
+ event_id TEXT PRIMARY KEY,
19761
+ created_at INTEGER NOT NULL,
19762
+ expires_at INTEGER
19763
+ )`);
19764
+ const pruneExpired = database.query(`DELETE FROM "${tableName}" WHERE expires_at IS NOT NULL AND expires_at <= ?1`);
19765
+ const select = database.query(`SELECT event_id FROM "${tableName}" WHERE event_id = ?1 AND (expires_at IS NULL OR expires_at > ?2) LIMIT 1`);
19766
+ const insert = database.query(`INSERT OR IGNORE INTO "${tableName}" (event_id, created_at, expires_at) VALUES (?1, ?2, ?3)`);
19767
+ const upsert = database.query(`INSERT INTO "${tableName}" (event_id, created_at, expires_at) VALUES (?1, ?2, ?3)
19768
+ ON CONFLICT(event_id) DO UPDATE SET expires_at = excluded.expires_at`);
19769
+ return {
19770
+ claim: (eventId) => {
19771
+ const now = Date.now();
19772
+ pruneExpired.run(now);
19773
+ const result = insert.run(eventId, now, getTelnyxEventExpiresAt(options.ttlSeconds));
19774
+ return result.changes > 0;
19775
+ },
19776
+ has: (eventId) => Boolean(select.get(eventId, Date.now())),
19777
+ set: (eventId) => {
19778
+ upsert.run(eventId, Date.now(), getTelnyxEventExpiresAt(options.ttlSeconds));
19779
+ }
19780
+ };
19781
+ };
19782
+ var createVoiceTelnyxPostgresClient = async (options) => {
19783
+ if (options.sql) {
19784
+ return options.sql;
19785
+ }
19786
+ if (!options.connectionString) {
19787
+ throw new Error("createVoicePostgresTelnyxWebhookEventStore requires either options.sql or options.connectionString.");
19788
+ }
19789
+ const sql = new Bun.SQL(options.connectionString);
19790
+ return {
19791
+ unsafe: sql.unsafe.bind(sql)
19792
+ };
19793
+ };
19794
+ var resolveTelnyxEventQualifiedTableName = (options) => {
19795
+ const schema = normalizeTelnyxStoreIdentifierSegment(options.schemaName ?? "public");
19796
+ const table = resolveTelnyxEventTableName({
19797
+ fallback: "telnyx_webhook_events",
19798
+ tableName: options.tableName,
19799
+ tablePrefix: options.tablePrefix
19800
+ });
19801
+ return `${quoteTelnyxStoreIdentifier(schema)}.${quoteTelnyxStoreIdentifier(table)}`;
19802
+ };
19803
+ var createVoicePostgresTelnyxWebhookEventStore = (options = {}) => {
19804
+ const qualifiedTableName = resolveTelnyxEventQualifiedTableName(options);
19805
+ const schemaMatch = qualifiedTableName.match(/^"([^"]+)"\./);
19806
+ const client = createVoiceTelnyxPostgresClient(options);
19807
+ const initialized = (async () => {
19808
+ const sql = await client;
19809
+ if (schemaMatch?.[1]) {
19810
+ await sql.unsafe(`CREATE SCHEMA IF NOT EXISTS ${quoteTelnyxStoreIdentifier(schemaMatch[1])}`);
19811
+ }
19812
+ await sql.unsafe(`CREATE TABLE IF NOT EXISTS ${qualifiedTableName} (
19813
+ event_id TEXT PRIMARY KEY,
19814
+ created_at BIGINT NOT NULL,
19815
+ expires_at BIGINT
19816
+ )`);
19817
+ })();
19818
+ const pruneExpired = async () => {
19819
+ await initialized;
19820
+ const sql = await client;
19821
+ await sql.unsafe(`DELETE FROM ${qualifiedTableName} WHERE expires_at IS NOT NULL AND expires_at <= $1`, [Date.now()]);
19822
+ };
19823
+ return {
19824
+ claim: async (eventId) => {
19825
+ await pruneExpired();
19826
+ const sql = await client;
19827
+ const rows = await sql.unsafe(`INSERT INTO ${qualifiedTableName} (event_id, created_at, expires_at)
19828
+ VALUES ($1, $2, $3)
19829
+ ON CONFLICT (event_id) DO NOTHING
19830
+ RETURNING event_id`, [eventId, Date.now(), getTelnyxEventExpiresAt(options.ttlSeconds)]);
19831
+ return rows.length > 0;
19832
+ },
19833
+ has: async (eventId) => {
19834
+ await initialized;
19835
+ const sql = await client;
19836
+ const rows = await sql.unsafe(`SELECT event_id FROM ${qualifiedTableName}
19837
+ WHERE event_id = $1 AND (expires_at IS NULL OR expires_at > $2)
19838
+ LIMIT 1`, [eventId, Date.now()]);
19839
+ return rows.length > 0;
19840
+ },
19841
+ set: async (eventId) => {
19842
+ await initialized;
19843
+ const sql = await client;
19844
+ await sql.unsafe(`INSERT INTO ${qualifiedTableName} (event_id, created_at, expires_at)
19845
+ VALUES ($1, $2, $3)
19846
+ ON CONFLICT (event_id) DO UPDATE SET expires_at = EXCLUDED.expires_at`, [eventId, Date.now(), getTelnyxEventExpiresAt(options.ttlSeconds)]);
19847
+ }
19848
+ };
19849
+ };
19850
+ var getTelnyxRedisEventKey = (keyPrefix, eventId) => `${keyPrefix}:${eventId}`;
19851
+ var createVoiceRedisTelnyxWebhookEventStore = (options = {}) => {
19852
+ const client = options.client ?? new Bun.RedisClient(options.url);
19853
+ const keyPrefix = options.keyPrefix?.trim() || "voice:telnyx-webhook-event";
19854
+ const ttlSeconds = options.ttlSeconds;
19855
+ const setEvent = async (eventId, nx) => {
19856
+ const key = getTelnyxRedisEventKey(keyPrefix, eventId);
19857
+ if (typeof ttlSeconds === "number" && ttlSeconds > 0) {
19858
+ return client.set(key, "1", "EX", String(Math.ceil(ttlSeconds)), ...nx ? ["NX"] : []);
19859
+ }
19860
+ return client.set(key, "1", ...nx ? ["NX"] : []);
19861
+ };
19862
+ return {
19863
+ claim: async (eventId) => await setEvent(eventId, true) === "OK",
19864
+ has: async (eventId) => Boolean(await client.exists(getTelnyxRedisEventKey(keyPrefix, eventId))),
19865
+ set: async (eventId) => {
19866
+ await setEvent(eventId, false);
19867
+ }
19868
+ };
19869
+ };
19870
+ var readTelnyxWebhookEventId = (rawBody) => {
19871
+ try {
19872
+ const body = JSON.parse(rawBody);
19873
+ if (!body || typeof body !== "object" || Array.isArray(body)) {
19874
+ return;
19875
+ }
19876
+ const record = body;
19877
+ const data = record.data;
19878
+ if (data && typeof data === "object" && !Array.isArray(data)) {
19879
+ const eventId2 = data.id;
19880
+ if (typeof eventId2 === "string" && eventId2.trim()) {
19881
+ return eventId2;
19882
+ }
19883
+ }
19884
+ const eventId = record.id ?? record.event_id;
19885
+ return typeof eventId === "string" && eventId.trim() ? eventId : undefined;
19886
+ } catch {
19887
+ return;
19888
+ }
19889
+ };
19890
+ var createVoiceTelnyxWebhookVerifier = (options) => async (input) => {
19891
+ const verification = await verifyVoiceTelnyxWebhookSignature({
19892
+ body: input.rawBody,
19893
+ headers: input.headers,
19894
+ publicKey: options.publicKey,
19895
+ toleranceSeconds: options.toleranceSeconds
19896
+ });
19897
+ if (!verification.ok) {
19898
+ return verification;
19899
+ }
19900
+ const eventStore = options.eventStore;
19901
+ if (!eventStore) {
19902
+ return verification;
19903
+ }
19904
+ const eventId = readTelnyxWebhookEventId(input.rawBody);
19905
+ if (!eventId) {
19906
+ return { ok: false, reason: "invalid-signature" };
19907
+ }
19908
+ if (eventStore.claim) {
19909
+ return await eventStore.claim(eventId) ? verification : { ok: false, reason: "invalid-signature" };
19910
+ }
19911
+ if (await eventStore.has(eventId)) {
19912
+ return { ok: false, reason: "invalid-signature" };
19913
+ }
19914
+ await eventStore.set(eventId);
19915
+ return verification;
19916
+ };
19578
19917
  var buildTelnyxVoiceSetupStatus = async (options, input) => {
19579
19918
  const origin = resolveRequestOrigin3(input.request);
19580
19919
  const stream = await resolveTelnyxStreamUrl(options, input);
@@ -19708,11 +20047,10 @@ var createTelnyxVoiceRoutes = (options = {}) => {
19708
20047
  const smokePath = options.smoke?.path === false ? false : options.smoke?.path ?? "/api/voice/telnyx/smoke";
19709
20048
  const bridges = new WeakMap;
19710
20049
  const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
19711
- const verify = options.webhook?.verify ?? (options.webhook?.publicKey ? (input) => verifyVoiceTelnyxWebhookSignature({
19712
- body: input.rawBody,
19713
- headers: input.headers,
19714
- publicKey: options.webhook?.publicKey,
19715
- toleranceSeconds: options.webhook?.toleranceSeconds
20050
+ const verify = options.webhook?.verify ?? (options.webhook?.publicKey ? createVoiceTelnyxWebhookVerifier({
20051
+ eventStore: options.webhook.eventStore,
20052
+ publicKey: options.webhook.publicKey,
20053
+ toleranceSeconds: options.webhook.toleranceSeconds
19716
20054
  }) : undefined);
19717
20055
  const app = new Elysia31({
19718
20056
  name: options.name ?? "absolutejs-voice-telnyx"
@@ -23844,7 +24182,7 @@ var createVoiceOpsRecoveryRoutes = (options = {}) => {
23844
24182
 
23845
24183
  // src/observabilityExport.ts
23846
24184
  import { Elysia as Elysia41 } from "elysia";
23847
- import { Database } from "bun:sqlite";
24185
+ import { Database as Database3 } from "bun:sqlite";
23848
24186
  import { createHash } from "crypto";
23849
24187
  import { mkdir as mkdir4, readFile as readFile2, stat, unlink } from "fs/promises";
23850
24188
  import { join as join3 } from "path";
@@ -25074,7 +25412,7 @@ var loadVoiceObservabilityExportReplaySource = async (source) => {
25074
25412
  if (!source.database && !source.path) {
25075
25413
  throw new Error("SQLite observability export replay requires source.database or source.path.");
25076
25414
  }
25077
- const database = source.database ?? new Database(source.path, { create: false });
25415
+ const database = source.database ?? new Database3(source.path, { create: false });
25078
25416
  const table2 = quoteObservabilityIdentifier(normalizeObservabilityIdentifier(source.tableName));
25079
25417
  const row2 = database.query(`SELECT manifest_json, artifact_index_json, payload_json FROM ${table2} WHERE run_id = $runId`).get({ $runId: source.runId });
25080
25418
  if (!row2) {
@@ -25151,7 +25489,7 @@ var deliverObservabilityExportToSQLite = async (input) => {
25151
25489
  if (!input.destination.database && !input.destination.path) {
25152
25490
  throw new Error("SQLite observability export delivery requires destination.database or destination.path.");
25153
25491
  }
25154
- const database = input.destination.database ?? new Database(input.destination.path, { create: true });
25492
+ const database = input.destination.database ?? new Database3(input.destination.path, { create: true });
25155
25493
  const table = quoteObservabilityIdentifier(normalizeObservabilityIdentifier(input.destination.tableName));
25156
25494
  const record = buildObservabilityExportDatabaseRecord(input);
25157
25495
  database.exec(`CREATE TABLE IF NOT EXISTS ${table} (
@@ -29261,7 +29599,7 @@ var createVoiceTraceDeliveryRoutes = (options) => {
29261
29599
  return routes;
29262
29600
  };
29263
29601
  // src/sqliteStore.ts
29264
- import { Database as Database2 } from "bun:sqlite";
29602
+ import { Database as Database4 } from "bun:sqlite";
29265
29603
  var normalizeTableNameSegment = (value) => value.trim().replace(/[^a-zA-Z0-9_]+/g, "_").replace(/^_+|_+$/g, "") || "voice";
29266
29604
  var resolveTableName = (input) => {
29267
29605
  if (input.options.tableName) {
@@ -29272,7 +29610,7 @@ var resolveTableName = (input) => {
29272
29610
  return `${prefix}_${fallback}`;
29273
29611
  };
29274
29612
  var openVoiceSQLiteDatabase = (path) => {
29275
- const database = new Database2(path, {
29613
+ const database = new Database4(path, {
29276
29614
  create: true
29277
29615
  });
29278
29616
  database.exec("PRAGMA journal_mode = WAL;");
@@ -31634,6 +31972,7 @@ export {
31634
31972
  createVoiceToolContractJSONHandler,
31635
31973
  createVoiceToolContractHTMLHandler,
31636
31974
  createVoiceToolContract,
31975
+ createVoiceTelnyxWebhookVerifier,
31637
31976
  createVoiceTelnyxCampaignDialer,
31638
31977
  createVoiceTelephonyWebhookRoutes,
31639
31978
  createVoiceTelephonyWebhookHandler,
@@ -31657,11 +31996,13 @@ export {
31657
31996
  createVoiceSTTProviderRouter,
31658
31997
  createVoiceSQLiteTraceSinkDeliveryStore,
31659
31998
  createVoiceSQLiteTraceEventStore,
31999
+ createVoiceSQLiteTelnyxWebhookEventStore,
31660
32000
  createVoiceSQLiteTelephonyWebhookIdempotencyStore,
31661
32001
  createVoiceSQLiteTaskStore,
31662
32002
  createVoiceSQLiteSessionStore,
31663
32003
  createVoiceSQLiteRuntimeStorage,
31664
32004
  createVoiceSQLiteReviewStore,
32005
+ createVoiceSQLitePlivoWebhookNonceStore,
31665
32006
  createVoiceSQLiteIntegrationEventStore,
31666
32007
  createVoiceSQLiteExternalObjectMapStore,
31667
32008
  createVoiceSQLiteDeliverySink,
@@ -31673,8 +32014,10 @@ export {
31673
32014
  createVoiceRoutingDecisionSummary,
31674
32015
  createVoiceReviewSavedEvent,
31675
32016
  createVoiceResilienceRoutes,
32017
+ createVoiceRedisTelnyxWebhookEventStore,
31676
32018
  createVoiceRedisTelephonyWebhookIdempotencyStore,
31677
32019
  createVoiceRedisTaskLeaseCoordinator,
32020
+ createVoiceRedisPlivoWebhookNonceStore,
31678
32021
  createVoiceRedisIdempotencyStore,
31679
32022
  createVoiceReconnectContractRoutes,
31680
32023
  createVoiceReadinessProfile,
@@ -31695,11 +32038,13 @@ export {
31695
32038
  createVoiceProductionReadinessRoutes,
31696
32039
  createVoicePostgresTraceSinkDeliveryStore,
31697
32040
  createVoicePostgresTraceEventStore,
32041
+ createVoicePostgresTelnyxWebhookEventStore,
31698
32042
  createVoicePostgresTelephonyWebhookIdempotencyStore,
31699
32043
  createVoicePostgresTaskStore,
31700
32044
  createVoicePostgresSessionStore,
31701
32045
  createVoicePostgresRuntimeStorage,
31702
32046
  createVoicePostgresReviewStore,
32047
+ createVoicePostgresPlivoWebhookNonceStore,
31703
32048
  createVoicePostgresIntegrationEventStore,
31704
32049
  createVoicePostgresExternalObjectMapStore,
31705
32050
  createVoicePostgresDeliverySink,
@@ -31849,6 +32194,7 @@ export {
31849
32194
  createOpenAIVoiceTTS,
31850
32195
  createOpenAIVoiceAssistantModel,
31851
32196
  createOpenAIRealtimeAdapter,
32197
+ createMemoryVoiceTelnyxWebhookEventStore,
31852
32198
  createMemoryVoiceTelephonyWebhookIdempotencyStore,
31853
32199
  createMemoryVoicePlivoWebhookNonceStore,
31854
32200
  createJSONVoiceAssistantModel,
@@ -1,7 +1,10 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { RedisClient } from 'bun';
1
3
  import { Elysia } from 'elysia';
2
4
  import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
3
5
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
4
6
  import type { VoiceServerMessage, VoiceSessionRecord } from '../types';
7
+ import type { VoicePostgresClient } from '../postgresStore';
5
8
  import { type TwilioMediaStreamBridgeOptions } from './twilio';
6
9
  export type PlivoInboundMessage = {
7
10
  event: 'start';
@@ -132,9 +135,32 @@ export type PlivoVoiceSmokeOptions = {
132
135
  title?: string;
133
136
  };
134
137
  export type VoicePlivoWebhookNonceStore = {
138
+ claim?: (nonce: string) => Promise<boolean> | boolean;
135
139
  has: (nonce: string) => Promise<boolean> | boolean;
136
140
  set: (nonce: string) => Promise<void> | void;
137
141
  };
142
+ export type VoicePlivoWebhookNonceStoreOptions = {
143
+ ttlSeconds?: number;
144
+ };
145
+ export type VoiceSQLitePlivoWebhookNonceStoreOptions = VoicePlivoWebhookNonceStoreOptions & {
146
+ database?: Database;
147
+ path?: string;
148
+ tableName?: string;
149
+ tablePrefix?: string;
150
+ };
151
+ export type VoicePostgresPlivoWebhookNonceStoreOptions = VoicePlivoWebhookNonceStoreOptions & {
152
+ connectionString?: string;
153
+ schemaName?: string;
154
+ sql?: VoicePostgresClient;
155
+ tableName?: string;
156
+ tablePrefix?: string;
157
+ };
158
+ export type VoiceRedisPlivoWebhookNonceClient = Pick<RedisClient, 'exists' | 'set'>;
159
+ export type VoiceRedisPlivoWebhookNonceStoreOptions = VoicePlivoWebhookNonceStoreOptions & {
160
+ client?: VoiceRedisPlivoWebhookNonceClient;
161
+ keyPrefix?: string;
162
+ url?: string;
163
+ };
138
164
  export type VoicePlivoWebhookVerifierOptions = {
139
165
  authToken?: string;
140
166
  nonceStore?: VoicePlivoWebhookNonceStore;
@@ -186,6 +212,9 @@ export declare const verifyVoicePlivoWebhookSignature: (input: {
186
212
  url: string;
187
213
  }) => Promise<VoiceTelephonyWebhookVerificationResult>;
188
214
  export declare const createMemoryVoicePlivoWebhookNonceStore: () => VoicePlivoWebhookNonceStore;
215
+ export declare const createVoiceSQLitePlivoWebhookNonceStore: (options: VoiceSQLitePlivoWebhookNonceStoreOptions) => VoicePlivoWebhookNonceStore;
216
+ export declare const createVoicePostgresPlivoWebhookNonceStore: (options?: VoicePostgresPlivoWebhookNonceStoreOptions) => VoicePlivoWebhookNonceStore;
217
+ export declare const createVoiceRedisPlivoWebhookNonceStore: (options?: VoiceRedisPlivoWebhookNonceStoreOptions) => VoicePlivoWebhookNonceStore;
189
218
  export declare const createVoicePlivoWebhookVerifier: (options: VoicePlivoWebhookVerifierOptions) => (input: {
190
219
  body: unknown;
191
220
  headers: Headers;
@@ -1,8 +1,11 @@
1
+ import { Database } from 'bun:sqlite';
2
+ import type { RedisClient } from 'bun';
1
3
  import { Elysia } from 'elysia';
2
4
  import { type VoiceTelephonyContractReport, type VoiceTelephonySetupStatus, type VoiceTelephonySmokeCheck, type VoiceTelephonySmokeReport } from './contract';
3
5
  import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions, type VoiceTelephonyWebhookVerificationResult } from '../telephonyOutcome';
4
6
  import type { VoiceServerMessage, VoiceSessionRecord } from '../types';
5
7
  import { type TwilioMediaStreamBridgeOptions } from './twilio';
8
+ import type { VoicePostgresClient } from '../postgresStore';
6
9
  export type TelnyxMediaPayload = {
7
10
  chunk?: string;
8
11
  payload: string;
@@ -151,12 +154,45 @@ export type TelnyxVoiceRoutesOptions<TContext = unknown, TSession extends VoiceS
151
154
  }) => Promise<string> | string);
152
155
  };
153
156
  webhook?: Omit<VoiceTelephonyWebhookRoutesOptions<TContext, TSession, TResult>, 'context' | 'path' | 'policy' | 'provider'> & {
157
+ eventStore?: VoiceTelnyxWebhookEventStore;
154
158
  path?: string;
155
159
  policy?: VoiceTelephonyOutcomePolicy;
156
160
  publicKey?: string;
157
161
  toleranceSeconds?: number;
158
162
  };
159
163
  };
164
+ export type VoiceTelnyxWebhookEventStore = {
165
+ claim?: (eventId: string) => Promise<boolean> | boolean;
166
+ has: (eventId: string) => Promise<boolean> | boolean;
167
+ set: (eventId: string) => Promise<void> | void;
168
+ };
169
+ export type VoiceTelnyxWebhookEventStoreOptions = {
170
+ ttlSeconds?: number;
171
+ };
172
+ export type VoiceSQLiteTelnyxWebhookEventStoreOptions = VoiceTelnyxWebhookEventStoreOptions & {
173
+ database?: Database;
174
+ path?: string;
175
+ tableName?: string;
176
+ tablePrefix?: string;
177
+ };
178
+ export type VoicePostgresTelnyxWebhookEventStoreOptions = VoiceTelnyxWebhookEventStoreOptions & {
179
+ connectionString?: string;
180
+ schemaName?: string;
181
+ sql?: VoicePostgresClient;
182
+ tableName?: string;
183
+ tablePrefix?: string;
184
+ };
185
+ export type VoiceRedisTelnyxWebhookEventClient = Pick<RedisClient, 'exists' | 'set'>;
186
+ export type VoiceRedisTelnyxWebhookEventStoreOptions = VoiceTelnyxWebhookEventStoreOptions & {
187
+ client?: VoiceRedisTelnyxWebhookEventClient;
188
+ keyPrefix?: string;
189
+ url?: string;
190
+ };
191
+ export type VoiceTelnyxWebhookVerifierOptions = {
192
+ eventStore?: VoiceTelnyxWebhookEventStore;
193
+ publicKey?: string;
194
+ toleranceSeconds?: number;
195
+ };
160
196
  export declare const createTelnyxVoiceResponse: (options: TelnyxVoiceResponseOptions) => string;
161
197
  export declare const createTelnyxMediaStreamBridge: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(socket: TelnyxMediaStreamSocket, options: TelnyxMediaStreamBridgeOptions<TContext, TSession, TResult>) => TelnyxMediaStreamBridge;
162
198
  export declare const verifyVoiceTelnyxWebhookSignature: (input: {
@@ -165,6 +201,14 @@ export declare const verifyVoiceTelnyxWebhookSignature: (input: {
165
201
  publicKey?: string;
166
202
  toleranceSeconds?: number;
167
203
  }) => Promise<VoiceTelephonyWebhookVerificationResult>;
204
+ export declare const createMemoryVoiceTelnyxWebhookEventStore: () => VoiceTelnyxWebhookEventStore;
205
+ export declare const createVoiceSQLiteTelnyxWebhookEventStore: (options: VoiceSQLiteTelnyxWebhookEventStoreOptions) => VoiceTelnyxWebhookEventStore;
206
+ export declare const createVoicePostgresTelnyxWebhookEventStore: (options?: VoicePostgresTelnyxWebhookEventStoreOptions) => VoiceTelnyxWebhookEventStore;
207
+ export declare const createVoiceRedisTelnyxWebhookEventStore: (options?: VoiceRedisTelnyxWebhookEventStoreOptions) => VoiceTelnyxWebhookEventStore;
208
+ export declare const createVoiceTelnyxWebhookVerifier: (options: VoiceTelnyxWebhookVerifierOptions) => (input: {
209
+ rawBody: string;
210
+ headers: Headers;
211
+ }) => Promise<VoiceTelephonyWebhookVerificationResult>;
168
212
  export declare const createTelnyxVoiceRoutes: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: TelnyxVoiceRoutesOptions<TContext, TSession, TResult>) => Elysia<"", {
169
213
  decorator: {};
170
214
  store: {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.270",
3
+ "version": "0.0.22-beta.272",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",