titan-agent 6.1.1 → 6.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/README.md +25 -5
  2. package/dist/config/schema.js +23 -0
  3. package/dist/config/schema.js.map +1 -1
  4. package/dist/gateway/routes/telephony.js +497 -0
  5. package/dist/gateway/routes/telephony.js.map +1 -0
  6. package/dist/gateway/server.js +6 -0
  7. package/dist/gateway/server.js.map +1 -1
  8. package/dist/skills/builtin/phone_call.js +66 -0
  9. package/dist/skills/builtin/phone_call.js.map +1 -0
  10. package/dist/skills/registry.js +2 -0
  11. package/dist/skills/registry.js.map +1 -1
  12. package/dist/telephony/dograhClient.js +175 -0
  13. package/dist/telephony/dograhClient.js.map +1 -0
  14. package/dist/telephony/optOutStore.js +71 -0
  15. package/dist/telephony/optOutStore.js.map +1 -0
  16. package/dist/telephony/phonePolicy.js +120 -0
  17. package/dist/telephony/phonePolicy.js.map +1 -0
  18. package/dist/utils/constants.js +1 -1
  19. package/dist/utils/constants.js.map +1 -1
  20. package/package.json +1 -1
  21. package/ui/dist/assets/AuditPanel-DuxwAH5U.js +1 -0
  22. package/ui/dist/assets/{AutonomyPanel-zsQ6kIP7.js → AutonomyPanel-VWBgurnv.js} +1 -1
  23. package/ui/dist/assets/{AutopilotPanel-NlxdRleV.js → AutopilotPanel-Dw8MYOtj.js} +1 -1
  24. package/ui/dist/assets/{AutoresearchPanel-CHeEcUmN.js → AutoresearchPanel-CpEX0Zub.js} +1 -1
  25. package/ui/dist/assets/{BackupPanel-C7RHr9Gu.js → BackupPanel-n41_6i6x.js} +1 -1
  26. package/ui/dist/assets/{BrowserPanel-Cxu9bCLW.js → BrowserPanel-DEBTIt6h.js} +1 -1
  27. package/ui/dist/assets/{CPActivity-B0QSTuSq.js → CPActivity-em82PTiQ.js} +1 -1
  28. package/ui/dist/assets/{CPAgentDetail-BUqGIkaM.js → CPAgentDetail-Bd8lKqcL.js} +1 -1
  29. package/ui/dist/assets/{CPAgents-DO0RVLLA.js → CPAgents-GRBrDd6c.js} +1 -1
  30. package/ui/dist/assets/{CPApprovals-DRceMgME.js → CPApprovals-DdLaCgJZ.js} +1 -1
  31. package/ui/dist/assets/{CPCosts-D9l4mkiW.js → CPCosts-Cm8Hv9fv.js} +1 -1
  32. package/ui/dist/assets/{CPDashboard-DaCCdMjs.js → CPDashboard-Dm9vUKLu.js} +1 -1
  33. package/ui/dist/assets/{CPFiles-DTphWHsR.js → CPFiles-DxMn1x2m.js} +1 -1
  34. package/ui/dist/assets/{CPGoals-DeJBRV3M.js → CPGoals-Wr1tCn6y.js} +1 -1
  35. package/ui/dist/assets/{CPInbox-DsT5O636.js → CPInbox-CGYpK1rG.js} +2 -2
  36. package/ui/dist/assets/{CPIssueDetail-D8pG5Tnq.js → CPIssueDetail-dTU59sgQ.js} +1 -1
  37. package/ui/dist/assets/{CPIssues-oeffaZSb.js → CPIssues-wgjI2eqS.js} +1 -1
  38. package/ui/dist/assets/CPLayout-CBtGEYSs.js +2 -0
  39. package/ui/dist/assets/{CPOrg-CesGXL8r.js → CPOrg-eT6LNmyy.js} +1 -1
  40. package/ui/dist/assets/{CPRuns-CfYgZUGv.js → CPRuns-B3sdo5g2.js} +1 -1
  41. package/ui/dist/assets/{CPSocial-CK0qGSa0.js → CPSocial-B99Mfee_.js} +3 -3
  42. package/ui/dist/assets/{CPVoice-iIkC-KnQ.js → CPVoice-D4J6yFWu.js} +1 -1
  43. package/ui/dist/assets/{ChannelsPanel-DTS5SXlC.js → ChannelsPanel--lhZqYLD.js} +1 -1
  44. package/ui/dist/assets/{CheckpointsPanel-c5dwo2eW.js → CheckpointsPanel-DaOIUcEi.js} +1 -1
  45. package/ui/dist/assets/{CommandPostHub-BsdfjoBN.js → CommandPostHub-Pq9lQMW5.js} +3 -3
  46. package/ui/dist/assets/{CronPanel-BkeP_mIS.js → CronPanel-CywjWM7O.js} +1 -1
  47. package/ui/dist/assets/{DataTable-Gajb24Ni.js → DataTable-Bt0cxehB.js} +1 -1
  48. package/ui/dist/assets/{DreamPanel-CGrozxjo.js → DreamPanel-Ct7MuLzD.js} +1 -1
  49. package/ui/dist/assets/{EmptyState-B3lWjJU-.js → EmptyState-iLOrbdU7.js} +1 -1
  50. package/ui/dist/assets/{EvalHarnessPanel-DQ4O9caO.js → EvalHarnessPanel-7KKDzeXw.js} +2 -2
  51. package/ui/dist/assets/{EvalPanel-BOUHXllR.js → EvalPanel-70LzkLDO.js} +1 -1
  52. package/ui/dist/assets/{FileViewer-00_4kM0_.js → FileViewer-h6A7YYTm.js} +1 -1
  53. package/ui/dist/assets/{FilesPanel-DISpHxEJ.js → FilesPanel-l8qC6eLD.js} +1 -1
  54. package/ui/dist/assets/{FleetPanel-Vz7_88zK.js → FleetPanel-Z3ResHIb.js} +1 -1
  55. package/ui/dist/assets/{HomelabPanel-CCqGyERB.js → HomelabPanel-9QhrXzo1.js} +1 -1
  56. package/ui/dist/assets/InfraView-G1q6vF0Q.js +2 -0
  57. package/ui/dist/assets/{InlineEditableField-BGjdtpGQ.js → InlineEditableField-DsVfGRH2.js} +1 -1
  58. package/ui/dist/assets/{Input-CzDjGc5Y.js → Input-Bzh0-cXU.js} +1 -1
  59. package/ui/dist/assets/{IntegrationsPanel-BOe9ybbQ.js → IntegrationsPanel-DBOdLYJ8.js} +1 -1
  60. package/ui/dist/assets/IntelligenceView-J8Nbh4zs.js +2 -0
  61. package/ui/dist/assets/{LearningPanel-5zb6yYZW.js → LearningPanel-CiSLVxUX.js} +1 -1
  62. package/ui/dist/assets/{LogsPanel-C_1bqy9b.js → LogsPanel-CPAAItsq.js} +1 -1
  63. package/ui/dist/assets/{McpPanel-CY7_7erZ.js → McpPanel-MYWE2Zqs.js} +1 -1
  64. package/ui/dist/assets/{MemoryGraphPanel-DsF8er1w.js → MemoryGraphPanel-BFr51D_1.js} +1 -1
  65. package/ui/dist/assets/{MemoryWikiPanel-D4KPpOX7.js → MemoryWikiPanel-BA7GUxhg.js} +1 -1
  66. package/ui/dist/assets/{MeshPanel-C_cNMUcI.js → MeshPanel-5BDbwBVR.js} +1 -1
  67. package/ui/dist/assets/{MissionCanvas-BMw5bJcw.js → MissionCanvas-CrDZIQX6.js} +1 -1
  68. package/ui/dist/assets/{MissionChat-CrgPUYhU.js → MissionChat-BDTWMQjj.js} +1 -1
  69. package/ui/dist/assets/{MissionLibrary-ASkOLTWA.js → MissionLibrary-DEPH2eEu.js} +1 -1
  70. package/ui/dist/assets/{MissionStart-DDoZMkie.js → MissionStart-COLFZLHB.js} +1 -1
  71. package/ui/dist/assets/{Modal-DTzLKnUQ.js → Modal-nvni-LGR.js} +1 -1
  72. package/ui/dist/assets/{NvidiaPanel-dtkQr6gS.js → NvidiaPanel-BCgVrdWt.js} +1 -1
  73. package/ui/dist/assets/{OrganismPanel-DLfHCoYh.js → OrganismPanel-Cab4MV9i.js} +1 -1
  74. package/ui/dist/assets/{OverviewPanel-COcMqu-y.js → OverviewPanel-isNUxBki.js} +1 -1
  75. package/ui/dist/assets/{PageHeader-BXEGoqDX.js → PageHeader-BJ1jHtOy.js} +1 -1
  76. package/ui/dist/assets/{PersonaProfilesPanel-BKWsHE1j.js → PersonaProfilesPanel-BGiDuDPj.js} +1 -1
  77. package/ui/dist/assets/{PersonasPanel-tsXvN_8B.js → PersonasPanel-4-USk8fm.js} +1 -1
  78. package/ui/dist/assets/PhoneDeskPanel-CUz5YVUY.js +11 -0
  79. package/ui/dist/assets/{RecipesPanel-B8GOxD4_.js → RecipesPanel-DJd6Otdy.js} +1 -1
  80. package/ui/dist/assets/{SecurityPanel-CqhRhXZ8.js → SecurityPanel-CKqTnhNY.js} +1 -1
  81. package/ui/dist/assets/{SelfImprovePanel-Di0XhdJJ.js → SelfImprovePanel-BiCjKpTM.js} +1 -1
  82. package/ui/dist/assets/{SelfProposalsPanel-CbgdHeQY.js → SelfProposalsPanel-BQCwEHdF.js} +1 -1
  83. package/ui/dist/assets/{SessionsPanel-DBdDx51G.js → SessionsPanel-MTMSODP7.js} +1 -1
  84. package/ui/dist/assets/{SessionsTab-C8cEt83E.js → SessionsTab-CEFFZWMK.js} +1 -1
  85. package/ui/dist/assets/{SettingsPanel-BAenxGtq.js → SettingsPanel-CniESkQQ.js} +1 -1
  86. package/ui/dist/assets/SettingsView-2AwXqlvc.js +2 -0
  87. package/ui/dist/assets/{SkeletonLoader-DGQ0Q7vy.js → SkeletonLoader-IpWkLmoy.js} +1 -1
  88. package/ui/dist/assets/{SkillsPanel-r91e0ype.js → SkillsPanel-CCP-gCR4.js} +1 -1
  89. package/ui/dist/assets/{SomaView-CGoJDjEX.js → SomaView-BK_thK9H.js} +1 -1
  90. package/ui/dist/assets/{StatCard-D0Q7T1EQ.js → StatCard-I0fVw2CH.js} +1 -1
  91. package/ui/dist/assets/{StatusBadge-Do8GEljT.js → StatusBadge-e3tFf05B.js} +1 -1
  92. package/ui/dist/assets/{Tabs-B-g5Ghos.js → Tabs-vGpPiBzr.js} +1 -1
  93. package/ui/dist/assets/{TeamsPanel-BrT1eigj.js → TeamsPanel-AFIQBzRK.js} +1 -1
  94. package/ui/dist/assets/{TelemetryPanel-C9vV_ydx.js → TelemetryPanel-DKZGVmcp.js} +1 -1
  95. package/ui/dist/assets/{TitanCanvas-BPVdmdYo.js → TitanCanvas-CqvuuIjd.js} +4 -4
  96. package/ui/dist/assets/{ToolsView-COEuSnLh.js → ToolsView-DnIkrUvP.js} +2 -2
  97. package/ui/dist/assets/{Tooltip-Ip86jr39.js → Tooltip-DNc5AWi0.js} +1 -1
  98. package/ui/dist/assets/{TraceViewer-BcK-DNNV.js → TraceViewer-MD9YRFVc.js} +1 -1
  99. package/ui/dist/assets/{TrainingPanel-s5yKr9vs.js → TrainingPanel-BfXoR7_N.js} +1 -1
  100. package/ui/dist/assets/{VoiceOverlay-BKtgGRK8.js → VoiceOverlay-Bl81c4yf.js} +1 -1
  101. package/ui/dist/assets/{VramPanel-C2vulpkc.js → VramPanel-Ba8H8Qs2.js} +1 -1
  102. package/ui/dist/assets/{WatchView-GXZc0v1H.js → WatchView-CoLHPGd7.js} +1 -1
  103. package/ui/dist/assets/{WorkTab-CCQpoRi6.js → WorkTab-UppfR_AF.js} +1 -1
  104. package/ui/dist/assets/{WorkflowsPanel-CjU5wOPV.js → WorkflowsPanel-CdN7Epxr.js} +1 -1
  105. package/ui/dist/assets/{archive-Cty0CkR8.js → archive-DWgNklp0.js} +1 -1
  106. package/ui/dist/assets/{arrow-left-pfqo1Fw_.js → arrow-left-CW5NV7E0.js} +1 -1
  107. package/ui/dist/assets/{book-open-B6YYAKPS.js → book-open-DRI_Y6cW.js} +1 -1
  108. package/ui/dist/assets/{bot-CF5xQRRg.js → bot-DSGZRM5O.js} +1 -1
  109. package/ui/dist/assets/{chart-column-DmdEaxIF.js → chart-column-D6yK9tts.js} +1 -1
  110. package/ui/dist/assets/{check-B06kj6lQ.js → check-Cxp-6q4k.js} +1 -1
  111. package/ui/dist/assets/{chevron-right-BANsqvxz.js → chevron-right-DkIMxdwH.js} +1 -1
  112. package/ui/dist/assets/{chevron-up-DZTojCej.js → chevron-up-BbDu8x1d.js} +1 -1
  113. package/ui/dist/assets/{circle-check-big-2NAZFdp9.js → circle-check-big-DnJPBAGU.js} +1 -1
  114. package/ui/dist/assets/{circle-stop-AU_Yx5I3.js → circle-stop-DOe0OacY.js} +1 -1
  115. package/ui/dist/assets/clipboard-list-Dj5RQbT8.js +6 -0
  116. package/ui/dist/assets/{clock-DXDS0v3F.js → clock-BS8fh7Rt.js} +1 -1
  117. package/ui/dist/assets/{dollar-sign-DLLEescF.js → dollar-sign-DMXBeaTj.js} +1 -1
  118. package/ui/dist/assets/{download-CsU2Repn.js → download-BykARpbG.js} +1 -1
  119. package/ui/dist/assets/{external-link-Chx9uvWw.js → external-link-BfXhMi9L.js} +1 -1
  120. package/ui/dist/assets/{file-text-RZVxniN-.js → file-text-CUkSytWL.js} +1 -1
  121. package/ui/dist/assets/{flask-conical-BMH8pJKb.js → flask-conical-BS4cuENu.js} +1 -1
  122. package/ui/dist/assets/{folder-DgwmNgPQ.js → folder-BDwoUobN.js} +1 -1
  123. package/ui/dist/assets/{funnel-BR7hTVZW.js → funnel-xr6Dd1ub.js} +1 -1
  124. package/ui/dist/assets/{git-branch-B5R0dCYG.js → git-branch-C0Qk52W8.js} +1 -1
  125. package/ui/dist/assets/{index-BUCi9exS.js → index-CmaGnHzR.js} +30 -30
  126. package/ui/dist/assets/index-OZm1iJy9.css +1 -0
  127. package/ui/dist/assets/{layers-DGEsxpYY.js → layers-DPHYdeRk.js} +1 -1
  128. package/ui/dist/assets/{legacy-DgcX067d.js → legacy-DzagVk5C.js} +1 -1
  129. package/ui/dist/assets/{lightbulb-BvNv5S2z.js → lightbulb-Bj6cj6Xl.js} +1 -1
  130. package/ui/dist/assets/{list-todo-BYS3IIni.js → list-todo-Cm4jNFHV.js} +1 -1
  131. package/ui/dist/assets/{message-square-I2Wa53bg.js → message-square-BrWLAovN.js} +1 -1
  132. package/ui/dist/assets/{mic-CLF69wwf.js → mic-BXNK4d7X.js} +1 -1
  133. package/ui/dist/assets/{missions-a23pAr7Z.js → missions-DEjMYSre.js} +1 -1
  134. package/ui/dist/assets/{network-DPPIz6Io.js → network-CrQn1GKv.js} +1 -1
  135. package/ui/dist/assets/{pause-4M6u54u8.js → pause-DS5xgDMT.js} +1 -1
  136. package/ui/dist/assets/{pencil-CmthDu_c.js → pencil-DAI6FtFQ.js} +1 -1
  137. package/ui/dist/assets/{play-C5er2jT3.js → play-DxBy837b.js} +1 -1
  138. package/ui/dist/assets/{plug-Cj409xHk.js → plug-D0Bstfm0.js} +1 -1
  139. package/ui/dist/assets/{plus-C63FIH_4.js → plus-DxgNIt8h.js} +1 -1
  140. package/ui/dist/assets/{proxy-CV0RFkNZ.js → proxy-KYomRnLW.js} +1 -1
  141. package/ui/dist/assets/{radio-DJcw-8Fl.js → radio-2vniRFTO.js} +1 -1
  142. package/ui/dist/assets/{refresh-cw-D7RrQJQl.js → refresh-cw-BFBnnWj8.js} +1 -1
  143. package/ui/dist/assets/{rotate-ccw-wDOKy6Sz.js → rotate-ccw-B9wWlKRc.js} +1 -1
  144. package/ui/dist/assets/{save-Cy8s5sjb.js → save--wCYre_u.js} +1 -1
  145. package/ui/dist/assets/{search-BXNHN7Ra.js → search-DOD1aOm8.js} +1 -1
  146. package/ui/dist/assets/{send-DU7Jy0ph.js → send-Bb57UxT7.js} +1 -1
  147. package/ui/dist/assets/{server-Cg6QHqG3.js → server-CBsCfofY.js} +1 -1
  148. package/ui/dist/assets/{shield-check-nn5B33Zk.js → shield-check-B24N03VU.js} +1 -1
  149. package/ui/dist/assets/{terminal-D3qHXhYo.js → terminal-CZ77Swtc.js} +1 -1
  150. package/ui/dist/assets/{test-tube-qD4BUs0u.js → test-tube-CkkvIu5j.js} +1 -1
  151. package/ui/dist/assets/{toggle-right-B8VL5BEg.js → toggle-right-DgphAedS.js} +1 -1
  152. package/ui/dist/assets/{trash-2-BscnSMjc.js → trash-2-C5KNXS-O.js} +1 -1
  153. package/ui/dist/assets/{trending-up-CusGuaj_.js → trending-up-B3H8xrGD.js} +1 -1
  154. package/ui/dist/assets/{trophy-SEdQm3z6.js → trophy-C-xSgHD2.js} +1 -1
  155. package/ui/dist/assets/{volume-2-lPPI8coL.js → volume-2-BXwRjeve.js} +1 -1
  156. package/ui/dist/assets/{wifi-off-Deo-Hgei.js → wifi-off-DOm__Zn6.js} +1 -1
  157. package/ui/dist/index.html +2 -2
  158. package/ui/dist/sw.js +1 -1
  159. package/ui/dist/assets/AuditPanel-Dln-nzxR.js +0 -6
  160. package/ui/dist/assets/CPLayout-DVvrBIqI.js +0 -2
  161. package/ui/dist/assets/InfraView-KVwWRhIp.js +0 -2
  162. package/ui/dist/assets/IntelligenceView-CGUmFDYY.js +0 -2
  163. package/ui/dist/assets/SettingsView-DqklKAud.js +0 -2
  164. package/ui/dist/assets/index-CUxC9gxc.css +0 -1
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env node
2
+ import { Router } from "express";
3
+ import { loadConfig as loadTitanConfig } from "../../config/config.js";
4
+ import { readReceipts, writeReceipt } from "../../receipts/store.js";
5
+ import { createApproval, listApprovals } from "../../agent/commandPost.js";
6
+ import {
7
+ createDograhClientFromConfig,
8
+ summarizeDograhConfig
9
+ } from "../../telephony/dograhClient.js";
10
+ import { hashPhoneForApproval, normalizePhoneNumber, redactPhoneNumber, validateOutboundCallRequest } from "../../telephony/phonePolicy.js";
11
+ import { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from "../../telephony/optOutStore.js";
12
+ function safeString(value, max = 160) {
13
+ if (typeof value !== "string") return void 0;
14
+ return value.replace(/[\r\n\t]+/g, " ").slice(0, max).trim() || void 0;
15
+ }
16
+ function safeBool(value) {
17
+ return value === true || value === "true";
18
+ }
19
+ function outboundResponseBody(policy) {
20
+ return {
21
+ toNumberRedacted: policy.toNumberRedacted,
22
+ workflowId: policy.workflowId,
23
+ purpose: policy.purpose,
24
+ mode: policy.mode,
25
+ requiresApproval: policy.requiresApproval
26
+ };
27
+ }
28
+ function safeActionId(value) {
29
+ if (typeof value !== "string") return void 0;
30
+ const trimmed = value.trim();
31
+ return /^[A-Za-z0-9_-]{8,80}$/.test(trimmed) ? trimmed : void 0;
32
+ }
33
+ function metaString(meta, key) {
34
+ const value = meta?.[key];
35
+ return typeof value === "string" ? value : void 0;
36
+ }
37
+ function verifyPriorApproval(approvalActionId, policy) {
38
+ if (!approvalActionId) {
39
+ return { ok: false, status: 403, error: "telephony_approval_required", message: "A matching approved Command Post approval is required before placing this outbound call." };
40
+ }
41
+ const approval = listApprovals().find((item) => item.id === approvalActionId && item.type === "custom");
42
+ if (!approval || approval.payload?.category !== "telephony_outbound_call") {
43
+ return { ok: false, status: 403, error: "telephony_approval_required", message: "No matching Command Post approval was found for this outbound call." };
44
+ }
45
+ if (approval.status !== "approved") {
46
+ return { ok: false, status: 403, error: "telephony_approval_not_approved", message: "The supplied outbound call approval has not been approved yet." };
47
+ }
48
+ const consumed = readReceipts({ kind: "approval_decision", limit: 1e3 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === "ok");
49
+ if (consumed) {
50
+ return { ok: false, status: 409, error: "telephony_approval_already_used", message: "The supplied approval request was already used." };
51
+ }
52
+ const payload = approval.payload ?? {};
53
+ const expected = {
54
+ mode: policy.mode,
55
+ workflowId: policy.workflowId,
56
+ toNumberRedacted: policy.toNumberRedacted,
57
+ toNumberHash: policy.toNumberHash,
58
+ toNumberOptOutHash: policy.toNumberOptOutHash,
59
+ purpose: policy.purpose
60
+ };
61
+ for (const [key, value] of Object.entries(expected)) {
62
+ const actual = payload[key];
63
+ if (value !== void 0 && actual !== value) {
64
+ return { ok: false, status: 409, error: "telephony_approval_mismatch", message: "The supplied approval request does not match this outbound call." };
65
+ }
66
+ }
67
+ return { ok: true };
68
+ }
69
+ const campaignReservations = /* @__PURE__ */ new Map();
70
+ function purgeOldCampaignReservations(now = Date.now()) {
71
+ const cutoff = now - 60 * 60 * 1e3;
72
+ for (const [id, ts] of campaignReservations.entries()) {
73
+ if (ts < cutoff) campaignReservations.delete(id);
74
+ }
75
+ }
76
+ function campaignCallsInLastHour(now = Date.now()) {
77
+ purgeOldCampaignReservations(now);
78
+ const cutoff = now - 60 * 60 * 1e3;
79
+ const completed = readReceipts({ kind: "tool_call", limit: 1e3 }).filter((receipt) => {
80
+ if (receipt.status !== "ok") return false;
81
+ if (metaString(receipt.meta, "mode") !== "campaign") return false;
82
+ const ts = Date.parse(receipt.ts);
83
+ return Number.isFinite(ts) && ts >= cutoff;
84
+ }).length;
85
+ return completed + campaignReservations.size;
86
+ }
87
+ function reserveCampaignCall() {
88
+ const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
89
+ campaignReservations.set(id, Date.now());
90
+ return id;
91
+ }
92
+ function releaseCampaignReservation(id) {
93
+ if (id) campaignReservations.delete(id);
94
+ }
95
+ function firstString(...values) {
96
+ for (const value of values) {
97
+ if (typeof value === "string" && value.trim()) return value.trim();
98
+ if (typeof value === "number" && Number.isFinite(value)) return String(value);
99
+ }
100
+ return void 0;
101
+ }
102
+ function numberListContains(numbers, candidate) {
103
+ if (!candidate) return false;
104
+ return numbers.some((number) => normalizePhoneNumber(number) === candidate);
105
+ }
106
+ function hasOptOutIntent(transcript, keywords) {
107
+ if (!transcript) return void 0;
108
+ const lower = transcript.toLowerCase();
109
+ return keywords.find((keyword) => {
110
+ const normalized = keyword.trim().toLowerCase();
111
+ return normalized.length > 0 && new RegExp(`\\b${normalized.replace(/[.*+?^${}()|[\\]\\]/g, "\\$&")}\\b`, "i").test(lower);
112
+ });
113
+ }
114
+ function sanitizeTelephonyText(value) {
115
+ if (typeof value === "string") {
116
+ return value.replace(/\+?\d[\d\s().-]{7,}\d/g, "[REDACTED_PHONE]").replace(/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer [REDACTED]").replace(/\b(?:api[_-]?key|authorization|token|secret)\b\s*[:=]\s*[^\s,;]+/gi, "$1=[REDACTED]").replace(/dg_[A-Za-z0-9._~+/=-]+/g, "[REDACTED_DOGRAH_KEY]").slice(0, 500);
117
+ }
118
+ if (Array.isArray(value)) return value.slice(0, 20).map(sanitizeTelephonyText);
119
+ if (value && typeof value === "object") {
120
+ const out = {};
121
+ for (const [key, nested] of Object.entries(value).slice(0, 50)) {
122
+ if (/api[_-]?key|authorization|token|secret|password/i.test(key)) {
123
+ out[key] = "[REDACTED]";
124
+ } else {
125
+ out[key] = sanitizeTelephonyText(nested);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ return value;
131
+ }
132
+ function verifyDograhWebhook(req, config) {
133
+ const expected = config.telephony.dograh.apiKey?.trim();
134
+ if (!expected) {
135
+ return { ok: false, status: 403, error: "dograh_webhook_secret_missing", message: "Dograh inbound webhooks require telephony.dograh.apiKey." };
136
+ }
137
+ const auth = typeof req.headers.authorization === "string" ? req.headers.authorization : "";
138
+ const bearer = auth.toLowerCase().startsWith("bearer ") ? auth.slice(7).trim() : "";
139
+ const headerToken = typeof req.headers["x-titan-telephony-token"] === "string" ? req.headers["x-titan-telephony-token"].trim() : "";
140
+ if (bearer !== expected && headerToken !== expected) {
141
+ return { ok: false, status: 401, error: "dograh_webhook_unauthorized", message: "Dograh inbound webhook authentication failed." };
142
+ }
143
+ return { ok: true };
144
+ }
145
+ function telephonyReceiptMeta(receipt) {
146
+ return {
147
+ actionId: receipt.action_id,
148
+ ts: receipt.ts,
149
+ kind: receipt.kind,
150
+ status: receipt.status,
151
+ summary: receipt.summary,
152
+ source: typeof receipt.meta?.source === "string" ? receipt.meta.source : void 0,
153
+ mode: typeof receipt.meta?.mode === "string" ? receipt.meta.mode : void 0,
154
+ direction: typeof receipt.meta?.direction === "string" ? receipt.meta.direction : void 0,
155
+ toNumberRedacted: typeof receipt.meta?.toNumberRedacted === "string" ? receipt.meta.toNumberRedacted : void 0,
156
+ fromNumberRedacted: typeof receipt.meta?.fromNumberRedacted === "string" ? receipt.meta.fromNumberRedacted : void 0,
157
+ workflowId: typeof receipt.meta?.workflowId === "string" ? receipt.meta.workflowId : void 0,
158
+ workflowRunId: receipt.meta?.workflowRunId,
159
+ callId: typeof receipt.meta?.callId === "string" ? receipt.meta.callId : void 0
160
+ };
161
+ }
162
+ function createTelephonyRouter(deps = {}) {
163
+ const router = Router();
164
+ const loadConfig = deps.loadConfig ?? loadTitanConfig;
165
+ const createClient = deps.createClient ?? createDograhClientFromConfig;
166
+ router.get("/dograh/status", async (_req, res) => {
167
+ try {
168
+ const config = loadConfig();
169
+ const telephony = config.telephony;
170
+ const client = createClient(telephony);
171
+ const health = await client.health();
172
+ res.json({
173
+ ok: Boolean(health.ok),
174
+ provider: telephony.provider,
175
+ config: summarizeDograhConfig(telephony),
176
+ dograh: sanitizeTelephonyText(health)
177
+ });
178
+ } catch (error) {
179
+ res.status(500).json({
180
+ ok: false,
181
+ error: "dograh_status_failed",
182
+ message: error.message
183
+ });
184
+ }
185
+ });
186
+ router.get("/dograh/workflows", async (req, res) => {
187
+ try {
188
+ const status = typeof req.query.status === "string" && req.query.status.trim() ? req.query.status.trim() : "active";
189
+ const config = loadConfig();
190
+ const client = createClient(config.telephony);
191
+ const workflows = await client.listWorkflowSummaries(status);
192
+ res.json({ workflows, count: workflows.length });
193
+ } catch (error) {
194
+ res.status(502).json({
195
+ error: "dograh_workflows_unavailable",
196
+ message: error.message
197
+ });
198
+ }
199
+ });
200
+ router.get("/dograh/calls", (req, res) => {
201
+ const limit = Number(req.query.limit ?? 100);
202
+ const calls = readReceipts({ limit: Number.isFinite(limit) ? Math.min(Math.max(Math.floor(limit), 1), 500) : 100 }).filter((receipt) => receipt.meta?.source === "dograh" || String(receipt.summary).toLowerCase().includes("dograh")).map(telephonyReceiptMeta);
203
+ res.json({ calls, count: calls.length });
204
+ });
205
+ router.get("/dograh/opt-outs", (_req, res) => {
206
+ const optOuts = listTelephonyOptOuts().map((entry) => ({
207
+ phoneRedacted: entry.phoneRedacted,
208
+ reason: entry.reason,
209
+ source: entry.source,
210
+ recordedAt: entry.recordedAt,
211
+ callId: entry.callId
212
+ }));
213
+ res.json({ optOuts, count: optOuts.length });
214
+ });
215
+ router.post("/dograh/inbound", (req, res) => {
216
+ try {
217
+ const config = loadConfig();
218
+ if (!config.telephony.enabled) {
219
+ res.status(403).json({ ok: false, error: "telephony_disabled", message: "Telephony is disabled." });
220
+ return;
221
+ }
222
+ const webhookAuth = verifyDograhWebhook(req, config);
223
+ if ("status" in webhookAuth) {
224
+ writeReceipt({
225
+ kind: "error",
226
+ status: "fail",
227
+ summary: "Dograh inbound webhook rejected: authentication failed",
228
+ meta: { source: "dograh", direction: "inbound", error: webhookAuth.error }
229
+ });
230
+ res.status(webhookAuth.status).json({ ok: false, error: webhookAuth.error, message: webhookAuth.message });
231
+ return;
232
+ }
233
+ const fromNumber = normalizePhoneNumber(firstString(req.body?.fromNumber, req.body?.from, req.body?.callerNumber, req.body?.caller));
234
+ const toNumber = normalizePhoneNumber(firstString(req.body?.toNumber, req.body?.to, req.body?.calledNumber, req.body?.called));
235
+ const fromNumberRedacted = redactPhoneNumber(fromNumber);
236
+ const toNumberRedacted = redactPhoneNumber(toNumber);
237
+ const transcript = safeString(firstString(req.body?.transcript, req.body?.message, req.body?.text), 500);
238
+ const callId = safeString(firstString(req.body?.callId, req.body?.call_id, req.body?.providerCallId, req.body?.provider_sid), 120);
239
+ const workflowRunId = firstString(req.body?.workflowRunId, req.body?.workflow_run_id, req.body?.runId);
240
+ if (!fromNumber || !toNumber) {
241
+ writeReceipt({
242
+ kind: "error",
243
+ status: "fail",
244
+ summary: "Dograh inbound call blocked: invalid phone metadata",
245
+ meta: { source: "dograh", direction: "inbound", fromNumberRedacted, toNumberRedacted, callId }
246
+ });
247
+ res.status(400).json({ ok: false, error: "telephony_invalid_inbound_call", message: "Inbound call requires valid E.164 caller and called numbers." });
248
+ return;
249
+ }
250
+ const isAdminLine = numberListContains(config.telephony.adminNumbers, toNumber);
251
+ const isPublicLine = numberListContains(config.telephony.publicNumbers, toNumber) || !isAdminLine;
252
+ const isAdminCaller = numberListContains(config.telephony.adminNumbers, fromNumber);
253
+ const mode = isAdminLine ? "admin" : "receptionist";
254
+ const optOutKeyword = hasOptOutIntent(transcript, config.telephony.optOutKeywords);
255
+ let optOutRecorded = false;
256
+ if (optOutKeyword && !isAdminLine) {
257
+ recordTelephonyOptOut({
258
+ phoneNumber: fromNumber,
259
+ phoneHash: hashPhoneForApproval(config.telephony, fromNumber),
260
+ reason: `Inbound caller said opt-out keyword: ${optOutKeyword}`,
261
+ callId
262
+ });
263
+ optOutRecorded = true;
264
+ }
265
+ if (isAdminLine && !isAdminCaller) {
266
+ writeReceipt({
267
+ kind: "approval_decision",
268
+ status: "fail",
269
+ summary: `Dograh admin caller rejected: ${fromNumberRedacted}`,
270
+ meta: { source: "dograh", direction: "inbound", mode, fromNumberRedacted, toNumberRedacted, callId, workflowRunId }
271
+ });
272
+ res.status(403).json({
273
+ ok: false,
274
+ error: "telephony_admin_caller_not_allowed",
275
+ message: "Caller is not authorized for the TITAN admin phone line.",
276
+ mode,
277
+ authorized: false,
278
+ fromNumberRedacted,
279
+ toNumberRedacted
280
+ });
281
+ return;
282
+ }
283
+ writeReceipt({
284
+ kind: "tool_call",
285
+ status: "ok",
286
+ summary: isAdminLine ? `Dograh admin call accepted: ${fromNumberRedacted}` : `Dograh receptionist call received: ${fromNumberRedacted}`,
287
+ meta: {
288
+ source: "dograh",
289
+ direction: "inbound",
290
+ mode,
291
+ fromNumberRedacted,
292
+ toNumberRedacted,
293
+ callId,
294
+ workflowRunId,
295
+ optOutRecorded,
296
+ transcriptSummary: transcript ? String(sanitizeTelephonyText(transcript)).slice(0, 120) : void 0
297
+ }
298
+ });
299
+ res.json({
300
+ ok: true,
301
+ mode: isAdminLine ? "admin" : "receptionist",
302
+ authorized: isAdminLine && isAdminCaller,
303
+ nextAction: isPublicLine && !isAdminLine ? "record_message" : "admin_control",
304
+ fromNumberRedacted,
305
+ toNumberRedacted,
306
+ optOutRecorded
307
+ });
308
+ } catch (error) {
309
+ writeReceipt({
310
+ kind: "error",
311
+ status: "fail",
312
+ summary: "Dograh inbound call route failed",
313
+ meta: { source: "dograh", direction: "inbound", error: error.message.slice(0, 200) }
314
+ });
315
+ res.status(500).json({ ok: false, error: "dograh_inbound_route_failed", message: error.message });
316
+ }
317
+ });
318
+ router.post("/dograh/call", async (req, res) => {
319
+ try {
320
+ const config = loadConfig();
321
+ const policy = validateOutboundCallRequest(config.telephony, req.body ?? {});
322
+ if (!policy.ok) {
323
+ writeReceipt({
324
+ kind: "error",
325
+ status: "fail",
326
+ summary: `Dograh outbound call blocked: ${policy.reason ?? "policy failure"}`,
327
+ meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted }
328
+ });
329
+ res.status(403).json({
330
+ ok: false,
331
+ error: "telephony_policy_blocked",
332
+ message: policy.reason,
333
+ call: { toNumberRedacted: policy.toNumberRedacted, mode: policy.mode }
334
+ });
335
+ return;
336
+ }
337
+ const approved = safeBool(req.body?.approved);
338
+ const approvalActionId = safeActionId(req.body?.approvalActionId);
339
+ const approvalNote = safeString(req.body?.approvalNote, 200);
340
+ const responseCall = outboundResponseBody(policy);
341
+ if (policy.mode === "campaign" && isTelephonyOptedOut(policy.toNumberHash, policy.toNumberOptOutHash)) {
342
+ writeReceipt({
343
+ kind: "error",
344
+ status: "fail",
345
+ summary: `Dograh campaign call blocked by opt-out: ${policy.toNumberRedacted}`,
346
+ meta: { source: "dograh", mode: policy.mode, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash }
347
+ });
348
+ res.status(403).json({
349
+ ok: false,
350
+ error: "telephony_policy_blocked",
351
+ message: "Destination number has opted out of campaign calls.",
352
+ call: responseCall
353
+ });
354
+ return;
355
+ }
356
+ if (policy.requiresApproval && !approved) {
357
+ const approval = createApproval({
358
+ type: "custom",
359
+ requestedBy: `phone-desk:${policy.mode}:${policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
360
+ payload: {
361
+ category: "telephony_outbound_call",
362
+ mode: policy.mode,
363
+ workflowId: policy.workflowId,
364
+ toNumberRedacted: policy.toNumberRedacted,
365
+ toNumberHash: policy.toNumberHash,
366
+ toNumberOptOutHash: policy.toNumberOptOutHash,
367
+ purpose: policy.purpose,
368
+ source: "dograh"
369
+ }
370
+ });
371
+ const receipt = writeReceipt({
372
+ kind: "approval_request",
373
+ status: "pending",
374
+ summary: `Dograh outbound call approval required: ${policy.toNumberRedacted}`,
375
+ meta: {
376
+ mode: policy.mode,
377
+ workflowId: policy.workflowId,
378
+ toNumberRedacted: policy.toNumberRedacted,
379
+ toNumberHash: policy.toNumberHash,
380
+ toNumberOptOutHash: policy.toNumberOptOutHash,
381
+ purpose: policy.purpose
382
+ }
383
+ });
384
+ res.status(202).json({
385
+ ok: false,
386
+ status: "pending_approval",
387
+ approvalActionId: approval.id,
388
+ receiptActionId: receipt.action_id,
389
+ call: responseCall
390
+ });
391
+ return;
392
+ }
393
+ if (policy.requiresApproval) {
394
+ const approval = verifyPriorApproval(approvalActionId, policy);
395
+ if ("status" in approval) {
396
+ res.status(approval.status).json({
397
+ ok: false,
398
+ error: approval.error,
399
+ message: approval.message,
400
+ call: responseCall
401
+ });
402
+ return;
403
+ }
404
+ }
405
+ let campaignReservationId;
406
+ if (policy.mode === "campaign") {
407
+ if (campaignCallsInLastHour() >= config.telephony.maxCallsPerHour) {
408
+ writeReceipt({
409
+ kind: "error",
410
+ status: "fail",
411
+ summary: `Dograh campaign call rate-limited: ${policy.toNumberRedacted}`,
412
+ meta: { mode: policy.mode, workflowId: policy.workflowId, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash }
413
+ });
414
+ res.status(429).json({
415
+ ok: false,
416
+ error: "telephony_rate_limited",
417
+ message: `Campaign outbound call limit reached: ${config.telephony.maxCallsPerHour}/hour.`,
418
+ call: responseCall
419
+ });
420
+ return;
421
+ }
422
+ campaignReservationId = reserveCampaignCall();
423
+ }
424
+ if (approved) {
425
+ writeReceipt({
426
+ kind: "approval_decision",
427
+ status: "ok",
428
+ parent_action_id: approvalActionId,
429
+ summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,
430
+ meta: {
431
+ mode: policy.mode,
432
+ workflowId: policy.workflowId,
433
+ toNumberRedacted: policy.toNumberRedacted,
434
+ toNumberHash: policy.toNumberHash,
435
+ note: approvalNote
436
+ }
437
+ });
438
+ }
439
+ const client = createClient(config.telephony);
440
+ let result;
441
+ try {
442
+ result = await client.initiateOutboundCall({
443
+ workflowId: policy.workflowId,
444
+ phoneNumber: policy.toNumber,
445
+ telephonyConfigurationId: policy.telephonyConfigurationId,
446
+ fromPhoneNumberId: policy.fromPhoneNumberId
447
+ });
448
+ } finally {
449
+ releaseCampaignReservation(campaignReservationId);
450
+ }
451
+ if (!result.ok) {
452
+ writeReceipt({
453
+ kind: "tool_call",
454
+ status: "fail",
455
+ summary: `Dograh outbound call failed: ${policy.toNumberRedacted}`,
456
+ meta: { status: result.status, error: result.error, workflowId: policy.workflowId }
457
+ });
458
+ res.status(502).json({
459
+ ok: false,
460
+ status: "failed",
461
+ error: "dograh_call_failed",
462
+ message: result.error,
463
+ call: { ...responseCall, workflowRunId: result.workflowRunId }
464
+ });
465
+ return;
466
+ }
467
+ writeReceipt({
468
+ kind: "tool_call",
469
+ status: "ok",
470
+ summary: `Dograh outbound call started: ${policy.toNumberRedacted}`,
471
+ meta: { workflowRunId: result.workflowRunId, workflowId: policy.workflowId, mode: policy.mode }
472
+ });
473
+ res.json({
474
+ ok: true,
475
+ status: "started",
476
+ call: { ...responseCall, workflowRunId: result.workflowRunId }
477
+ });
478
+ } catch (error) {
479
+ writeReceipt({
480
+ kind: "error",
481
+ status: "fail",
482
+ summary: "Dograh outbound call route failed",
483
+ meta: { error: error.message.slice(0, 200) }
484
+ });
485
+ res.status(500).json({
486
+ ok: false,
487
+ error: "dograh_call_route_failed",
488
+ message: error.message
489
+ });
490
+ }
491
+ });
492
+ return router;
493
+ }
494
+ export {
495
+ createTelephonyRouter
496
+ };
497
+ //# sourceMappingURL=telephony.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/gateway/routes/telephony.ts"],"sourcesContent":["import { Router, type Request } from 'express';\nimport { loadConfig as loadTitanConfig } from '../../config/config.js';\nimport type { TitanConfig } from '../../config/schema.js';\nimport { readReceipts, writeReceipt } from '../../receipts/store.js';\nimport { createApproval, listApprovals } from '../../agent/commandPost.js';\nimport {\n createDograhClientFromConfig,\n summarizeDograhConfig,\n type DograhOutboundCallRequest,\n type DograhOutboundCallResult,\n type DograhWorkflowSummary,\n} from '../../telephony/dograhClient.js';\nimport { hashPhoneForApproval, normalizePhoneNumber, redactPhoneNumber, validateOutboundCallRequest } from '../../telephony/phonePolicy.js';\nimport { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from '../../telephony/optOutStore.js';\n\ninterface DograhRouteClient {\n health: () => Promise<unknown>;\n listWorkflowSummaries: (status?: string) => Promise<DograhWorkflowSummary[]>;\n initiateOutboundCall: (call: DograhOutboundCallRequest) => Promise<DograhOutboundCallResult>;\n}\n\nexport interface TelephonyRouterDeps {\n loadConfig?: () => TitanConfig;\n createClient?: (config: TitanConfig['telephony']) => DograhRouteClient;\n}\n\nfunction safeString(value: unknown, max = 160): string | undefined {\n if (typeof value !== 'string') return undefined;\n return value.replace(/[\\r\\n\\t]+/g, ' ').slice(0, max).trim() || undefined;\n}\n\nfunction safeBool(value: unknown): boolean {\n return value === true || value === 'true';\n}\n\nfunction outboundResponseBody(policy: ReturnType<typeof validateOutboundCallRequest>) {\n return {\n toNumberRedacted: policy.toNumberRedacted,\n workflowId: policy.workflowId,\n purpose: policy.purpose,\n mode: policy.mode,\n requiresApproval: policy.requiresApproval,\n };\n}\n\nfunction safeActionId(value: unknown): string | undefined {\n if (typeof value !== 'string') return undefined;\n const trimmed = value.trim();\n return /^[A-Za-z0-9_-]{8,80}$/.test(trimmed) ? trimmed : undefined;\n}\n\nfunction metaString(meta: Record<string, unknown> | undefined, key: string): string | undefined {\n const value = meta?.[key];\n return typeof value === 'string' ? value : undefined;\n}\n\nfunction verifyPriorApproval(approvalActionId: string | undefined, policy: ReturnType<typeof validateOutboundCallRequest>):\n | { ok: true }\n | { ok: false; status: number; error: string; message: string } {\n if (!approvalActionId) {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'A matching approved Command Post approval is required before placing this outbound call.' };\n }\n const approval = listApprovals().find((item) => item.id === approvalActionId && item.type === 'custom');\n if (!approval || approval.payload?.category !== 'telephony_outbound_call') {\n return { ok: false, status: 403, error: 'telephony_approval_required', message: 'No matching Command Post approval was found for this outbound call.' };\n }\n if (approval.status !== 'approved') {\n return { ok: false, status: 403, error: 'telephony_approval_not_approved', message: 'The supplied outbound call approval has not been approved yet.' };\n }\n const consumed = readReceipts({ kind: 'approval_decision', limit: 1000 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === 'ok');\n if (consumed) {\n return { ok: false, status: 409, error: 'telephony_approval_already_used', message: 'The supplied approval request was already used.' };\n }\n const payload = approval.payload ?? {};\n const expected: Record<string, string | undefined> = {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n };\n for (const [key, value] of Object.entries(expected)) {\n const actual = payload[key];\n if (value !== undefined && actual !== value) {\n return { ok: false, status: 409, error: 'telephony_approval_mismatch', message: 'The supplied approval request does not match this outbound call.' };\n }\n }\n return { ok: true };\n}\n\nconst campaignReservations = new Map<string, number>();\n\nfunction purgeOldCampaignReservations(now = Date.now()): void {\n const cutoff = now - 60 * 60 * 1000;\n for (const [id, ts] of campaignReservations.entries()) {\n if (ts < cutoff) campaignReservations.delete(id);\n }\n}\n\nfunction campaignCallsInLastHour(now = Date.now()): number {\n purgeOldCampaignReservations(now);\n const cutoff = now - 60 * 60 * 1000;\n const completed = readReceipts({ kind: 'tool_call', limit: 1000 }).filter((receipt) => {\n if (receipt.status !== 'ok') return false;\n if (metaString(receipt.meta, 'mode') !== 'campaign') return false;\n const ts = Date.parse(receipt.ts);\n return Number.isFinite(ts) && ts >= cutoff;\n }).length;\n return completed + campaignReservations.size;\n}\n\nfunction reserveCampaignCall(): string {\n const id = `${Date.now()}-${Math.random().toString(36).slice(2)}`;\n campaignReservations.set(id, Date.now());\n return id;\n}\n\nfunction releaseCampaignReservation(id: string | undefined): void {\n if (id) campaignReservations.delete(id);\n}\n\nfunction firstString(...values: unknown[]): string | undefined {\n for (const value of values) {\n if (typeof value === 'string' && value.trim()) return value.trim();\n if (typeof value === 'number' && Number.isFinite(value)) return String(value);\n }\n return undefined;\n}\n\nfunction numberListContains(numbers: string[], candidate: string | null): boolean {\n if (!candidate) return false;\n return numbers.some((number) => normalizePhoneNumber(number) === candidate);\n}\n\nfunction hasOptOutIntent(transcript: string | undefined, keywords: string[]): string | undefined {\n if (!transcript) return undefined;\n const lower = transcript.toLowerCase();\n return keywords.find((keyword) => {\n const normalized = keyword.trim().toLowerCase();\n return normalized.length > 0 && new RegExp(`\\\\b${normalized.replace(/[.*+?^${}()|[\\\\]\\\\]/g, '\\\\$&')}\\\\b`, 'i').test(lower);\n });\n}\n\nfunction sanitizeTelephonyText(value: unknown): unknown {\n if (typeof value === 'string') {\n return value\n .replace(/\\+?\\d[\\d\\s().-]{7,}\\d/g, '[REDACTED_PHONE]')\n .replace(/Bearer\\s+[A-Za-z0-9._~+/=-]+/gi, 'Bearer [REDACTED]')\n .replace(/\\b(?:api[_-]?key|authorization|token|secret)\\b\\s*[:=]\\s*[^\\s,;]+/gi, '$1=[REDACTED]')\n .replace(/dg_[A-Za-z0-9._~+/=-]+/g, '[REDACTED_DOGRAH_KEY]')\n .slice(0, 500);\n }\n if (Array.isArray(value)) return value.slice(0, 20).map(sanitizeTelephonyText);\n if (value && typeof value === 'object') {\n const out: Record<string, unknown> = {};\n for (const [key, nested] of Object.entries(value as Record<string, unknown>).slice(0, 50)) {\n if (/api[_-]?key|authorization|token|secret|password/i.test(key)) {\n out[key] = '[REDACTED]';\n } else {\n out[key] = sanitizeTelephonyText(nested);\n }\n }\n return out;\n }\n return value;\n}\n\nfunction verifyDograhWebhook(req: Request, config: TitanConfig): { ok: true } | { ok: false; status: number; error: string; message: string } {\n const expected = config.telephony.dograh.apiKey?.trim();\n if (!expected) {\n return { ok: false, status: 403, error: 'dograh_webhook_secret_missing', message: 'Dograh inbound webhooks require telephony.dograh.apiKey.' };\n }\n const auth = typeof req.headers.authorization === 'string' ? req.headers.authorization : '';\n const bearer = auth.toLowerCase().startsWith('bearer ') ? auth.slice(7).trim() : '';\n const headerToken = typeof req.headers['x-titan-telephony-token'] === 'string' ? req.headers['x-titan-telephony-token'].trim() : '';\n if (bearer !== expected && headerToken !== expected) {\n return { ok: false, status: 401, error: 'dograh_webhook_unauthorized', message: 'Dograh inbound webhook authentication failed.' };\n }\n return { ok: true };\n}\n\nfunction telephonyReceiptMeta(receipt: ReturnType<typeof readReceipts>[number]): Record<string, unknown> {\n return {\n actionId: receipt.action_id,\n ts: receipt.ts,\n kind: receipt.kind,\n status: receipt.status,\n summary: receipt.summary,\n source: typeof receipt.meta?.source === 'string' ? receipt.meta.source : undefined,\n mode: typeof receipt.meta?.mode === 'string' ? receipt.meta.mode : undefined,\n direction: typeof receipt.meta?.direction === 'string' ? receipt.meta.direction : undefined,\n toNumberRedacted: typeof receipt.meta?.toNumberRedacted === 'string' ? receipt.meta.toNumberRedacted : undefined,\n fromNumberRedacted: typeof receipt.meta?.fromNumberRedacted === 'string' ? receipt.meta.fromNumberRedacted : undefined,\n workflowId: typeof receipt.meta?.workflowId === 'string' ? receipt.meta.workflowId : undefined,\n workflowRunId: receipt.meta?.workflowRunId,\n callId: typeof receipt.meta?.callId === 'string' ? receipt.meta.callId : undefined,\n };\n}\n\nexport function createTelephonyRouter(deps: TelephonyRouterDeps = {}): Router {\n const router = Router();\n const loadConfig = deps.loadConfig ?? loadTitanConfig;\n const createClient = deps.createClient ?? createDograhClientFromConfig;\n\n router.get('/dograh/status', async (_req, res) => {\n try {\n const config = loadConfig();\n const telephony = config.telephony;\n const client = createClient(telephony);\n const health = await client.health();\n res.json({\n ok: Boolean((health as { ok?: boolean }).ok),\n provider: telephony.provider,\n config: summarizeDograhConfig(telephony),\n dograh: sanitizeTelephonyText(health),\n });\n } catch (error) {\n res.status(500).json({\n ok: false,\n error: 'dograh_status_failed',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/workflows', async (req, res) => {\n try {\n const status = typeof req.query.status === 'string' && req.query.status.trim()\n ? req.query.status.trim()\n : 'active';\n const config = loadConfig();\n const client = createClient(config.telephony);\n const workflows = await client.listWorkflowSummaries(status);\n res.json({ workflows, count: workflows.length });\n } catch (error) {\n res.status(502).json({\n error: 'dograh_workflows_unavailable',\n message: (error as Error).message,\n });\n }\n });\n\n router.get('/dograh/calls', (req, res) => {\n const limit = Number(req.query.limit ?? 100);\n const calls = readReceipts({ limit: Number.isFinite(limit) ? Math.min(Math.max(Math.floor(limit), 1), 500) : 100 })\n .filter((receipt) => receipt.meta?.source === 'dograh' || String(receipt.summary).toLowerCase().includes('dograh'))\n .map(telephonyReceiptMeta);\n res.json({ calls, count: calls.length });\n });\n\n router.get('/dograh/opt-outs', (_req, res) => {\n const optOuts = listTelephonyOptOuts().map((entry) => ({\n phoneRedacted: entry.phoneRedacted,\n reason: entry.reason,\n source: entry.source,\n recordedAt: entry.recordedAt,\n callId: entry.callId,\n }));\n res.json({ optOuts, count: optOuts.length });\n });\n\n router.post('/dograh/inbound', (req, res) => {\n try {\n const config = loadConfig();\n if (!config.telephony.enabled) {\n res.status(403).json({ ok: false, error: 'telephony_disabled', message: 'Telephony is disabled.' });\n return;\n }\n\n const webhookAuth = verifyDograhWebhook(req, config);\n if ('status' in webhookAuth) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound webhook rejected: authentication failed',\n meta: { source: 'dograh', direction: 'inbound', error: webhookAuth.error },\n });\n res.status(webhookAuth.status).json({ ok: false, error: webhookAuth.error, message: webhookAuth.message });\n return;\n }\n\n const fromNumber = normalizePhoneNumber(firstString(req.body?.fromNumber, req.body?.from, req.body?.callerNumber, req.body?.caller));\n const toNumber = normalizePhoneNumber(firstString(req.body?.toNumber, req.body?.to, req.body?.calledNumber, req.body?.called));\n const fromNumberRedacted = redactPhoneNumber(fromNumber);\n const toNumberRedacted = redactPhoneNumber(toNumber);\n const transcript = safeString(firstString(req.body?.transcript, req.body?.message, req.body?.text), 500);\n const callId = safeString(firstString(req.body?.callId, req.body?.call_id, req.body?.providerCallId, req.body?.provider_sid), 120);\n const workflowRunId = firstString(req.body?.workflowRunId, req.body?.workflow_run_id, req.body?.runId);\n\n if (!fromNumber || !toNumber) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call blocked: invalid phone metadata',\n meta: { source: 'dograh', direction: 'inbound', fromNumberRedacted, toNumberRedacted, callId },\n });\n res.status(400).json({ ok: false, error: 'telephony_invalid_inbound_call', message: 'Inbound call requires valid E.164 caller and called numbers.' });\n return;\n }\n\n const isAdminLine = numberListContains(config.telephony.adminNumbers, toNumber);\n const isPublicLine = numberListContains(config.telephony.publicNumbers, toNumber) || !isAdminLine;\n const isAdminCaller = numberListContains(config.telephony.adminNumbers, fromNumber);\n const mode = isAdminLine ? 'admin' : 'receptionist';\n const optOutKeyword = hasOptOutIntent(transcript, config.telephony.optOutKeywords);\n let optOutRecorded = false;\n if (optOutKeyword && !isAdminLine) {\n recordTelephonyOptOut({\n phoneNumber: fromNumber,\n phoneHash: hashPhoneForApproval(config.telephony, fromNumber),\n reason: `Inbound caller said opt-out keyword: ${optOutKeyword}`,\n callId,\n });\n optOutRecorded = true;\n }\n\n if (isAdminLine && !isAdminCaller) {\n writeReceipt({\n kind: 'approval_decision',\n status: 'fail',\n summary: `Dograh admin caller rejected: ${fromNumberRedacted}`,\n meta: { source: 'dograh', direction: 'inbound', mode, fromNumberRedacted, toNumberRedacted, callId, workflowRunId },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_admin_caller_not_allowed',\n message: 'Caller is not authorized for the TITAN admin phone line.',\n mode,\n authorized: false,\n fromNumberRedacted,\n toNumberRedacted,\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: isAdminLine\n ? `Dograh admin call accepted: ${fromNumberRedacted}`\n : `Dograh receptionist call received: ${fromNumberRedacted}`,\n meta: {\n source: 'dograh',\n direction: 'inbound',\n mode,\n fromNumberRedacted,\n toNumberRedacted,\n callId,\n workflowRunId,\n optOutRecorded,\n transcriptSummary: transcript ? String(sanitizeTelephonyText(transcript)).slice(0, 120) : undefined,\n },\n });\n\n res.json({\n ok: true,\n mode: isAdminLine ? 'admin' : 'receptionist',\n authorized: isAdminLine && isAdminCaller,\n nextAction: isPublicLine && !isAdminLine ? 'record_message' : 'admin_control',\n fromNumberRedacted,\n toNumberRedacted,\n optOutRecorded,\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh inbound call route failed',\n meta: { source: 'dograh', direction: 'inbound', error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({ ok: false, error: 'dograh_inbound_route_failed', message: (error as Error).message });\n }\n });\n\n router.post('/dograh/call', async (req, res) => {\n try {\n const config = loadConfig();\n const policy = validateOutboundCallRequest(config.telephony, req.body ?? {});\n if (!policy.ok) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh outbound call blocked: ${policy.reason ?? 'policy failure'}`,\n meta: { mode: policy.mode, toNumberRedacted: policy.toNumberRedacted },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: policy.reason,\n call: { toNumberRedacted: policy.toNumberRedacted, mode: policy.mode },\n });\n return;\n }\n\n const approved = safeBool(req.body?.approved);\n const approvalActionId = safeActionId(req.body?.approvalActionId);\n const approvalNote = safeString(req.body?.approvalNote, 200);\n const responseCall = outboundResponseBody(policy);\n\n if (policy.mode === 'campaign' && isTelephonyOptedOut(policy.toNumberHash, policy.toNumberOptOutHash)) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call blocked by opt-out: ${policy.toNumberRedacted}`,\n meta: { source: 'dograh', mode: policy.mode, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(403).json({\n ok: false,\n error: 'telephony_policy_blocked',\n message: 'Destination number has opted out of campaign calls.',\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval && !approved) {\n const approval = createApproval({\n type: 'custom',\n requestedBy: `phone-desk:${policy.mode}:${policy.toNumberHash?.slice(0, 12) ?? 'unknown'}`,\n payload: {\n category: 'telephony_outbound_call',\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n source: 'dograh',\n },\n });\n const receipt = writeReceipt({\n kind: 'approval_request',\n status: 'pending',\n summary: `Dograh outbound call approval required: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n toNumberOptOutHash: policy.toNumberOptOutHash,\n purpose: policy.purpose,\n },\n });\n res.status(202).json({\n ok: false,\n status: 'pending_approval',\n approvalActionId: approval.id,\n receiptActionId: receipt.action_id,\n call: responseCall,\n });\n return;\n }\n\n if (policy.requiresApproval) {\n const approval = verifyPriorApproval(approvalActionId, policy);\n if ('status' in approval) {\n res.status(approval.status).json({\n ok: false,\n error: approval.error,\n message: approval.message,\n call: responseCall,\n });\n return;\n }\n }\n\n let campaignReservationId: string | undefined;\n if (policy.mode === 'campaign') {\n if (campaignCallsInLastHour() >= config.telephony.maxCallsPerHour) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: `Dograh campaign call rate-limited: ${policy.toNumberRedacted}`,\n meta: { mode: policy.mode, workflowId: policy.workflowId, toNumberRedacted: policy.toNumberRedacted, toNumberHash: policy.toNumberHash },\n });\n res.status(429).json({\n ok: false,\n error: 'telephony_rate_limited',\n message: `Campaign outbound call limit reached: ${config.telephony.maxCallsPerHour}/hour.`,\n call: responseCall,\n });\n return;\n }\n campaignReservationId = reserveCampaignCall();\n }\n\n if (approved) {\n writeReceipt({\n kind: 'approval_decision',\n status: 'ok',\n parent_action_id: approvalActionId,\n summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,\n meta: {\n mode: policy.mode,\n workflowId: policy.workflowId,\n toNumberRedacted: policy.toNumberRedacted,\n toNumberHash: policy.toNumberHash,\n note: approvalNote,\n },\n });\n }\n\n const client = createClient(config.telephony);\n let result: DograhOutboundCallResult;\n try {\n result = await client.initiateOutboundCall({\n workflowId: policy.workflowId!,\n phoneNumber: policy.toNumber!,\n telephonyConfigurationId: policy.telephonyConfigurationId,\n fromPhoneNumberId: policy.fromPhoneNumberId,\n });\n } finally {\n releaseCampaignReservation(campaignReservationId);\n }\n\n if (!result.ok) {\n writeReceipt({\n kind: 'tool_call',\n status: 'fail',\n summary: `Dograh outbound call failed: ${policy.toNumberRedacted}`,\n meta: { status: result.status, error: result.error, workflowId: policy.workflowId },\n });\n res.status(502).json({\n ok: false,\n status: 'failed',\n error: 'dograh_call_failed',\n message: result.error,\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n return;\n }\n\n writeReceipt({\n kind: 'tool_call',\n status: 'ok',\n summary: `Dograh outbound call started: ${policy.toNumberRedacted}`,\n meta: { workflowRunId: result.workflowRunId, workflowId: policy.workflowId, mode: policy.mode },\n });\n res.json({\n ok: true,\n status: 'started',\n call: { ...responseCall, workflowRunId: result.workflowRunId },\n });\n } catch (error) {\n writeReceipt({\n kind: 'error',\n status: 'fail',\n summary: 'Dograh outbound call route failed',\n meta: { error: (error as Error).message.slice(0, 200) },\n });\n res.status(500).json({\n ok: false,\n error: 'dograh_call_route_failed',\n message: (error as Error).message,\n });\n }\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
@@ -87,6 +87,7 @@ import { createMeshRouter } from "./routes/mesh.js";
87
87
  import { createTestsRouter } from "./routes/tests.js";
88
88
  import { createSystemRouter } from "./routes/systemRouter.js";
89
89
  import { createVoiceRouter } from "./routes/voiceRouter.js";
90
+ import { createTelephonyRouter } from "./routes/telephony.js";
90
91
  import { createSocialRouter } from "./routes/socialRouter.js";
91
92
  import { createWatchRouter } from "./routes/watchRouter.js";
92
93
  import { setupSSEFlush } from "../utils/sseFlush.js";
@@ -998,6 +999,10 @@ async function startGateway(options) {
998
999
  next();
999
1000
  return;
1000
1001
  }
1002
+ if (req.path === "/telephony/dograh/inbound") {
1003
+ next();
1004
+ return;
1005
+ }
1001
1006
  const header = req.headers.authorization;
1002
1007
  const token = header?.startsWith("Bearer ") ? header.slice(7) : req.query.token;
1003
1008
  if (isValidToken(token, cfg)) {
@@ -1718,6 +1723,7 @@ async function startGateway(options) {
1718
1723
  onUserMessage: (id, content) => missionHandleUserMessage(id, content),
1719
1724
  onStatusChange: (id, status) => missionHandleStatusChange(id, status)
1720
1725
  }));
1726
+ app.use("/api/telephony", createTelephonyRouter());
1721
1727
  reattachMissionBridgesOnStartup().catch((err) => {
1722
1728
  logger.warn("Gateway", `Mission bridge re-attach on boot failed: ${err.message}`);
1723
1729
  });