titan-agent 6.2.0 → 6.2.2

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 (157) hide show
  1. package/README.md +4 -4
  2. package/dist/agent/commandPost.js +3 -2
  3. package/dist/agent/commandPost.js.map +1 -1
  4. package/dist/gateway/routes/telephony.js +75 -19
  5. package/dist/gateway/routes/telephony.js.map +1 -1
  6. package/dist/gateway/routes/update.js +100 -0
  7. package/dist/gateway/routes/update.js.map +1 -0
  8. package/dist/gateway/server.js +2 -65
  9. package/dist/gateway/server.js.map +1 -1
  10. package/dist/skills/builtin/phone_call.js +23 -1
  11. package/dist/skills/builtin/phone_call.js.map +1 -1
  12. package/dist/telephony/phonePolicy.js +19 -3
  13. package/dist/telephony/phonePolicy.js.map +1 -1
  14. package/dist/utils/constants.js +1 -1
  15. package/dist/utils/constants.js.map +1 -1
  16. package/package.json +1 -1
  17. package/ui/dist/assets/{AuditPanel-DuxwAH5U.js → AuditPanel-BmFAKA7p.js} +1 -1
  18. package/ui/dist/assets/{AutonomyPanel-VWBgurnv.js → AutonomyPanel-BzuCEsiG.js} +1 -1
  19. package/ui/dist/assets/{AutopilotPanel-Dw8MYOtj.js → AutopilotPanel-DA1vFEl4.js} +1 -1
  20. package/ui/dist/assets/{AutoresearchPanel-CpEX0Zub.js → AutoresearchPanel-BeUCXYtX.js} +1 -1
  21. package/ui/dist/assets/{BackupPanel-n41_6i6x.js → BackupPanel-B6N1pfMT.js} +1 -1
  22. package/ui/dist/assets/{BrowserPanel-DEBTIt6h.js → BrowserPanel-BGHaPi3O.js} +1 -1
  23. package/ui/dist/assets/{CPActivity-em82PTiQ.js → CPActivity-BUSwjFj2.js} +1 -1
  24. package/ui/dist/assets/{CPAgentDetail-Bd8lKqcL.js → CPAgentDetail-DlUEVujH.js} +1 -1
  25. package/ui/dist/assets/{CPAgents-GRBrDd6c.js → CPAgents-CnRmsSMo.js} +1 -1
  26. package/ui/dist/assets/{CPApprovals-DdLaCgJZ.js → CPApprovals-CXdkzebd.js} +1 -1
  27. package/ui/dist/assets/{CPCosts-Cm8Hv9fv.js → CPCosts-CfcyIUZg.js} +1 -1
  28. package/ui/dist/assets/{CPDashboard-Dm9vUKLu.js → CPDashboard-DHlfE1mT.js} +1 -1
  29. package/ui/dist/assets/{CPFiles-DxMn1x2m.js → CPFiles-DyV9vsjE.js} +1 -1
  30. package/ui/dist/assets/{CPGoals-Wr1tCn6y.js → CPGoals-C17G2F0e.js} +1 -1
  31. package/ui/dist/assets/{CPInbox-CGYpK1rG.js → CPInbox-D5A3ja_Q.js} +1 -1
  32. package/ui/dist/assets/{CPIssueDetail-dTU59sgQ.js → CPIssueDetail-CoPv27Ll.js} +1 -1
  33. package/ui/dist/assets/{CPIssues-wgjI2eqS.js → CPIssues-C9EdfW1J.js} +1 -1
  34. package/ui/dist/assets/CPLayout-CHrlktMs.js +2 -0
  35. package/ui/dist/assets/{CPOrg-eT6LNmyy.js → CPOrg-DzoVDLKI.js} +1 -1
  36. package/ui/dist/assets/{CPRuns-B3sdo5g2.js → CPRuns-BHAyFv0K.js} +1 -1
  37. package/ui/dist/assets/{CPSocial-B99Mfee_.js → CPSocial-CjHD8dKM.js} +1 -1
  38. package/ui/dist/assets/{CPVoice-D4J6yFWu.js → CPVoice-aybMotU5.js} +1 -1
  39. package/ui/dist/assets/{ChannelsPanel--lhZqYLD.js → ChannelsPanel-8KfvAEYM.js} +1 -1
  40. package/ui/dist/assets/{CheckpointsPanel-DaOIUcEi.js → CheckpointsPanel-BCyLRLcg.js} +1 -1
  41. package/ui/dist/assets/{CommandPostHub-Pq9lQMW5.js → CommandPostHub-Ce-mWsu2.js} +3 -3
  42. package/ui/dist/assets/{CronPanel-CywjWM7O.js → CronPanel-A9JHvU_O.js} +1 -1
  43. package/ui/dist/assets/{DataTable-Bt0cxehB.js → DataTable-ol6IZMRj.js} +1 -1
  44. package/ui/dist/assets/{DreamPanel-Ct7MuLzD.js → DreamPanel-ZiOF8ybh.js} +1 -1
  45. package/ui/dist/assets/{EmptyState-iLOrbdU7.js → EmptyState-B1i1ZK0x.js} +1 -1
  46. package/ui/dist/assets/{EvalHarnessPanel-7KKDzeXw.js → EvalHarnessPanel-DRGc1zbp.js} +1 -1
  47. package/ui/dist/assets/{EvalPanel-70LzkLDO.js → EvalPanel-B_fiFImG.js} +1 -1
  48. package/ui/dist/assets/{FileViewer-h6A7YYTm.js → FileViewer-D14G6MQq.js} +1 -1
  49. package/ui/dist/assets/{FilesPanel-l8qC6eLD.js → FilesPanel-BIsMoSfg.js} +1 -1
  50. package/ui/dist/assets/{FleetPanel-Z3ResHIb.js → FleetPanel-CSjOOsbX.js} +1 -1
  51. package/ui/dist/assets/{HomelabPanel-9QhrXzo1.js → HomelabPanel-XHMxqiHu.js} +1 -1
  52. package/ui/dist/assets/InfraView-DNYgRXkB.js +2 -0
  53. package/ui/dist/assets/{InlineEditableField-DsVfGRH2.js → InlineEditableField-AOS-LBh_.js} +1 -1
  54. package/ui/dist/assets/{Input-Bzh0-cXU.js → Input-CTK9oH9g.js} +1 -1
  55. package/ui/dist/assets/{IntegrationsPanel-DBOdLYJ8.js → IntegrationsPanel-BKuC_5El.js} +1 -1
  56. package/ui/dist/assets/IntelligenceView-BUKpVpDQ.js +2 -0
  57. package/ui/dist/assets/{LearningPanel-CiSLVxUX.js → LearningPanel-RpDaPvCj.js} +1 -1
  58. package/ui/dist/assets/{LogsPanel-CPAAItsq.js → LogsPanel-9zIJP1Xv.js} +1 -1
  59. package/ui/dist/assets/{McpPanel-MYWE2Zqs.js → McpPanel-CNE7MhDu.js} +1 -1
  60. package/ui/dist/assets/{MemoryGraphPanel-BFr51D_1.js → MemoryGraphPanel-9DEsbOZF.js} +1 -1
  61. package/ui/dist/assets/{MemoryWikiPanel-BA7GUxhg.js → MemoryWikiPanel-CqNen9A6.js} +1 -1
  62. package/ui/dist/assets/{MeshPanel-5BDbwBVR.js → MeshPanel-Bq3pHHIT.js} +1 -1
  63. package/ui/dist/assets/{MissionCanvas-CrDZIQX6.js → MissionCanvas-Ci0hbAF0.js} +1 -1
  64. package/ui/dist/assets/{MissionChat-BDTWMQjj.js → MissionChat-BDqlCplQ.js} +1 -1
  65. package/ui/dist/assets/{MissionLibrary-DEPH2eEu.js → MissionLibrary-9SMdJnfa.js} +1 -1
  66. package/ui/dist/assets/{MissionStart-COLFZLHB.js → MissionStart-Cm7C3kyo.js} +1 -1
  67. package/ui/dist/assets/{Modal-nvni-LGR.js → Modal-B9KCAwCK.js} +1 -1
  68. package/ui/dist/assets/{NvidiaPanel-BCgVrdWt.js → NvidiaPanel-CpGBj1sn.js} +1 -1
  69. package/ui/dist/assets/{OrganismPanel-Cab4MV9i.js → OrganismPanel-DBHyylxC.js} +1 -1
  70. package/ui/dist/assets/{OverviewPanel-isNUxBki.js → OverviewPanel-DII4H_R7.js} +1 -1
  71. package/ui/dist/assets/{PageHeader-BJ1jHtOy.js → PageHeader-Bzb_WXw0.js} +1 -1
  72. package/ui/dist/assets/{PersonaProfilesPanel-BGiDuDPj.js → PersonaProfilesPanel-CXZTZhjo.js} +1 -1
  73. package/ui/dist/assets/{PersonasPanel-4-USk8fm.js → PersonasPanel-BYbD8XiV.js} +1 -1
  74. package/ui/dist/assets/{PhoneDeskPanel-CUz5YVUY.js → PhoneDeskPanel-Bol7fCCN.js} +1 -1
  75. package/ui/dist/assets/{RecipesPanel-DJd6Otdy.js → RecipesPanel-BRbXRcEn.js} +1 -1
  76. package/ui/dist/assets/{SecurityPanel-CKqTnhNY.js → SecurityPanel-BR3BT7Rb.js} +1 -1
  77. package/ui/dist/assets/{SelfImprovePanel-BiCjKpTM.js → SelfImprovePanel-BX9xYBxl.js} +1 -1
  78. package/ui/dist/assets/{SelfProposalsPanel-BQCwEHdF.js → SelfProposalsPanel-BOoDEAYp.js} +1 -1
  79. package/ui/dist/assets/{SessionsPanel-MTMSODP7.js → SessionsPanel-BICnNmn0.js} +1 -1
  80. package/ui/dist/assets/{SessionsTab-CEFFZWMK.js → SessionsTab-X10FeDWA.js} +1 -1
  81. package/ui/dist/assets/{SettingsPanel-CniESkQQ.js → SettingsPanel-MuC32AXw.js} +1 -1
  82. package/ui/dist/assets/SettingsView-Cv9IZqiU.js +2 -0
  83. package/ui/dist/assets/{SkeletonLoader-IpWkLmoy.js → SkeletonLoader-fwI4Z-f0.js} +1 -1
  84. package/ui/dist/assets/{SkillsPanel-CCP-gCR4.js → SkillsPanel-CKFndekU.js} +1 -1
  85. package/ui/dist/assets/{SomaView-BK_thK9H.js → SomaView-Ch7WlmBy.js} +1 -1
  86. package/ui/dist/assets/{StatCard-I0fVw2CH.js → StatCard-Bj2Q_4WS.js} +1 -1
  87. package/ui/dist/assets/{StatusBadge-e3tFf05B.js → StatusBadge-NtdTQ8_L.js} +1 -1
  88. package/ui/dist/assets/{Tabs-vGpPiBzr.js → Tabs-CC9aTVuL.js} +1 -1
  89. package/ui/dist/assets/{TeamsPanel-AFIQBzRK.js → TeamsPanel-DjFcLsdR.js} +1 -1
  90. package/ui/dist/assets/{TelemetryPanel-DKZGVmcp.js → TelemetryPanel-k7gJbZyS.js} +1 -1
  91. package/ui/dist/assets/{TitanCanvas-CqvuuIjd.js → TitanCanvas-MzLkpZhp.js} +25 -25
  92. package/ui/dist/assets/{ToolsView-DnIkrUvP.js → ToolsView-DcxwuM2l.js} +2 -2
  93. package/ui/dist/assets/{Tooltip-DNc5AWi0.js → Tooltip-CFYHfpgQ.js} +1 -1
  94. package/ui/dist/assets/{TraceViewer-MD9YRFVc.js → TraceViewer-BfLO9vwI.js} +1 -1
  95. package/ui/dist/assets/{TrainingPanel-BfXoR7_N.js → TrainingPanel-DUl6qChY.js} +1 -1
  96. package/ui/dist/assets/{VoiceOverlay-Bl81c4yf.js → VoiceOverlay-Dj_cR5_Y.js} +1 -1
  97. package/ui/dist/assets/{VramPanel-Ba8H8Qs2.js → VramPanel-th0_Eo8o.js} +1 -1
  98. package/ui/dist/assets/{WatchView-CoLHPGd7.js → WatchView-DyGhpKbv.js} +1 -1
  99. package/ui/dist/assets/{WorkTab-UppfR_AF.js → WorkTab-BG5VBljJ.js} +1 -1
  100. package/ui/dist/assets/{WorkflowsPanel-CdN7Epxr.js → WorkflowsPanel-C1hVHqAc.js} +1 -1
  101. package/ui/dist/assets/{archive-DWgNklp0.js → archive-C2_hAJXj.js} +1 -1
  102. package/ui/dist/assets/{arrow-left-CW5NV7E0.js → arrow-left-DFCBXvKS.js} +1 -1
  103. package/ui/dist/assets/{book-open-DRI_Y6cW.js → book-open-BgDKDGcH.js} +1 -1
  104. package/ui/dist/assets/{bot-DSGZRM5O.js → bot-C4ICYImG.js} +1 -1
  105. package/ui/dist/assets/{chart-column-D6yK9tts.js → chart-column-BIFJOG_f.js} +1 -1
  106. package/ui/dist/assets/{check-Cxp-6q4k.js → check-CmCL8yzl.js} +1 -1
  107. package/ui/dist/assets/{chevron-right-DkIMxdwH.js → chevron-right-U0Ujye5i.js} +1 -1
  108. package/ui/dist/assets/{chevron-up-BbDu8x1d.js → chevron-up-B6uSBxbj.js} +1 -1
  109. package/ui/dist/assets/{circle-check-big-DnJPBAGU.js → circle-check-big-BFw1nom4.js} +1 -1
  110. package/ui/dist/assets/{circle-stop-DOe0OacY.js → circle-stop-DbtGpdaw.js} +1 -1
  111. package/ui/dist/assets/{clipboard-list-Dj5RQbT8.js → clipboard-list-Dd3_U8cK.js} +1 -1
  112. package/ui/dist/assets/{clock-BS8fh7Rt.js → clock-DpJIc30t.js} +1 -1
  113. package/ui/dist/assets/{dollar-sign-DMXBeaTj.js → dollar-sign-DlrOHVzA.js} +1 -1
  114. package/ui/dist/assets/{download-BykARpbG.js → download-CZ5kTONh.js} +1 -1
  115. package/ui/dist/assets/{external-link-BfXhMi9L.js → external-link-CX-GvMky.js} +1 -1
  116. package/ui/dist/assets/{file-text-CUkSytWL.js → file-text-BtTsNJbB.js} +1 -1
  117. package/ui/dist/assets/{flask-conical-BS4cuENu.js → flask-conical-BaPYppKH.js} +1 -1
  118. package/ui/dist/assets/{folder-BDwoUobN.js → folder-BOnpha2-.js} +1 -1
  119. package/ui/dist/assets/{funnel-xr6Dd1ub.js → funnel-DaNi6kwp.js} +1 -1
  120. package/ui/dist/assets/{git-branch-C0Qk52W8.js → git-branch-gs6g9dSe.js} +1 -1
  121. package/ui/dist/assets/{index-CmaGnHzR.js → index-Dbv5oKsp.js} +2 -2
  122. package/ui/dist/assets/{layers-DPHYdeRk.js → layers-BKZz8iD1.js} +1 -1
  123. package/ui/dist/assets/{legacy-DzagVk5C.js → legacy-DbwFsW2A.js} +1 -1
  124. package/ui/dist/assets/{lightbulb-Bj6cj6Xl.js → lightbulb-DX6YLtPe.js} +1 -1
  125. package/ui/dist/assets/{list-todo-Cm4jNFHV.js → list-todo-qdMNm4-Q.js} +1 -1
  126. package/ui/dist/assets/{message-square-BrWLAovN.js → message-square-Cwo5FOho.js} +1 -1
  127. package/ui/dist/assets/{mic-BXNK4d7X.js → mic-BzvwK4IS.js} +1 -1
  128. package/ui/dist/assets/{missions-DEjMYSre.js → missions-CYlXuJED.js} +1 -1
  129. package/ui/dist/assets/{network-CrQn1GKv.js → network-Dt-RBtO_.js} +1 -1
  130. package/ui/dist/assets/{pause-DS5xgDMT.js → pause-Do3XNLtx.js} +1 -1
  131. package/ui/dist/assets/{pencil-DAI6FtFQ.js → pencil-DAGs9RFs.js} +1 -1
  132. package/ui/dist/assets/{play-DxBy837b.js → play-CNElkzEq.js} +1 -1
  133. package/ui/dist/assets/{plug-D0Bstfm0.js → plug-DCgrVnB8.js} +1 -1
  134. package/ui/dist/assets/{plus-DxgNIt8h.js → plus-BnsXhqXc.js} +1 -1
  135. package/ui/dist/assets/{proxy-KYomRnLW.js → proxy-BXHdgwrk.js} +1 -1
  136. package/ui/dist/assets/{radio-2vniRFTO.js → radio-BPz7sHXW.js} +1 -1
  137. package/ui/dist/assets/{refresh-cw-BFBnnWj8.js → refresh-cw-Cs4oDMNE.js} +1 -1
  138. package/ui/dist/assets/{rotate-ccw-B9wWlKRc.js → rotate-ccw-DSpsA3Wr.js} +1 -1
  139. package/ui/dist/assets/{save--wCYre_u.js → save-BhGg2pZh.js} +1 -1
  140. package/ui/dist/assets/{search-DOD1aOm8.js → search-BtxlIgqN.js} +1 -1
  141. package/ui/dist/assets/{send-Bb57UxT7.js → send-BzKTXI3q.js} +1 -1
  142. package/ui/dist/assets/{server-CBsCfofY.js → server-B1gIgixS.js} +1 -1
  143. package/ui/dist/assets/{shield-check-B24N03VU.js → shield-check-C39B-Wi3.js} +1 -1
  144. package/ui/dist/assets/{terminal-CZ77Swtc.js → terminal-CubaeYlJ.js} +1 -1
  145. package/ui/dist/assets/{test-tube-CkkvIu5j.js → test-tube-CYYLfFIz.js} +1 -1
  146. package/ui/dist/assets/{toggle-right-DgphAedS.js → toggle-right-BQ3iDgLG.js} +1 -1
  147. package/ui/dist/assets/{trash-2-C5KNXS-O.js → trash-2-CSxfLiVB.js} +1 -1
  148. package/ui/dist/assets/{trending-up-B3H8xrGD.js → trending-up-DXxKT18Q.js} +1 -1
  149. package/ui/dist/assets/{trophy-C-xSgHD2.js → trophy-osEOm2Vi.js} +1 -1
  150. package/ui/dist/assets/{volume-2-BXwRjeve.js → volume-2-C1OkTurz.js} +1 -1
  151. package/ui/dist/assets/{wifi-off-DOm__Zn6.js → wifi-off-DAhvsjnD.js} +1 -1
  152. package/ui/dist/index.html +1 -1
  153. package/ui/dist/sw.js +1 -1
  154. package/ui/dist/assets/CPLayout-CBtGEYSs.js +0 -2
  155. package/ui/dist/assets/InfraView-G1q6vF0Q.js +0 -2
  156. package/ui/dist/assets/IntelligenceView-J8Nbh4zs.js +0 -2
  157. package/ui/dist/assets/SettingsView-2AwXqlvc.js +0 -2
@@ -1,4 +1,7 @@
1
1
  #!/usr/bin/env node
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join } from "path";
2
5
  import { Router } from "express";
3
6
  import { loadConfig as loadTitanConfig } from "../../config/config.js";
4
7
  import { readReceipts, writeReceipt } from "../../receipts/store.js";
@@ -7,7 +10,12 @@ import {
7
10
  createDograhClientFromConfig,
8
11
  summarizeDograhConfig
9
12
  } from "../../telephony/dograhClient.js";
10
- import { hashPhoneForApproval, normalizePhoneNumber, redactPhoneNumber, validateOutboundCallRequest } from "../../telephony/phonePolicy.js";
13
+ import {
14
+ hashPhoneForApproval,
15
+ normalizePhoneNumber,
16
+ redactPhoneNumber,
17
+ validateOutboundCallRequest
18
+ } from "../../telephony/phonePolicy.js";
11
19
  import { isTelephonyOptedOut, listTelephonyOptOuts, recordTelephonyOptOut } from "../../telephony/optOutStore.js";
12
20
  function safeString(value, max = 160) {
13
21
  if (typeof value !== "string") return void 0;
@@ -34,6 +42,38 @@ function metaString(meta, key) {
34
42
  const value = meta?.[key];
35
43
  return typeof value === "string" ? value : void 0;
36
44
  }
45
+ function telephonyStateHome() {
46
+ const envHome = process.env.TITAN_HOME?.trim();
47
+ if (envHome) {
48
+ if (envHome === "~") return homedir();
49
+ if (envHome.startsWith("~/")) return join(homedir(), envHome.slice(2));
50
+ return envHome;
51
+ }
52
+ return join(homedir(), ".titan");
53
+ }
54
+ function consumedApprovalsPath() {
55
+ return join(telephonyStateHome(), "telephony-consumed-approvals.json");
56
+ }
57
+ function readConsumedApprovalIds() {
58
+ const path = consumedApprovalsPath();
59
+ if (!existsSync(path)) return /* @__PURE__ */ new Set();
60
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
61
+ if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
62
+ return new Set(parsed.filter((value) => typeof value === "string"));
63
+ }
64
+ function isTelephonyApprovalConsumed(approvalActionId) {
65
+ if (readConsumedApprovalIds().has(approvalActionId)) return true;
66
+ return readReceipts({ kind: "approval_decision", limit: 1e3 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === "ok");
67
+ }
68
+ function consumeTelephonyApproval(approvalActionId) {
69
+ const path = consumedApprovalsPath();
70
+ const consumed = readConsumedApprovalIds();
71
+ if (consumed.has(approvalActionId)) return false;
72
+ consumed.add(approvalActionId);
73
+ mkdirSync(dirname(path), { recursive: true });
74
+ writeFileSync(path, JSON.stringify([...consumed].sort(), null, 2), "utf8");
75
+ return true;
76
+ }
37
77
  function verifyPriorApproval(approvalActionId, policy) {
38
78
  if (!approvalActionId) {
39
79
  return { ok: false, status: 403, error: "telephony_approval_required", message: "A matching approved Command Post approval is required before placing this outbound call." };
@@ -45,7 +85,7 @@ function verifyPriorApproval(approvalActionId, policy) {
45
85
  if (approval.status !== "approved") {
46
86
  return { ok: false, status: 403, error: "telephony_approval_not_approved", message: "The supplied outbound call approval has not been approved yet." };
47
87
  }
48
- const consumed = readReceipts({ kind: "approval_decision", limit: 1e3 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === "ok");
88
+ const consumed = isTelephonyApprovalConsumed(approvalActionId);
49
89
  if (consumed) {
50
90
  return { ok: false, status: 409, error: "telephony_approval_already_used", message: "The supplied approval request was already used." };
51
91
  }
@@ -56,6 +96,7 @@ function verifyPriorApproval(approvalActionId, policy) {
56
96
  toNumberRedacted: policy.toNumberRedacted,
57
97
  toNumberHash: policy.toNumberHash,
58
98
  toNumberOptOutHash: policy.toNumberOptOutHash,
99
+ approvalIdentity: policy.approvalIdentity,
59
100
  purpose: policy.purpose
60
101
  };
61
102
  for (const [key, value] of Object.entries(expected)) {
@@ -163,6 +204,7 @@ function createTelephonyRouter(deps = {}) {
163
204
  const router = Router();
164
205
  const loadConfig = deps.loadConfig ?? loadTitanConfig;
165
206
  const createClient = deps.createClient ?? createDograhClientFromConfig;
207
+ const consumeApproval = deps.consumeApproval ?? consumeTelephonyApproval;
166
208
  router.get("/dograh/status", async (_req, res) => {
167
209
  try {
168
210
  const config = loadConfig();
@@ -356,9 +398,13 @@ function createTelephonyRouter(deps = {}) {
356
398
  if (policy.requiresApproval && !approved) {
357
399
  const approval = createApproval({
358
400
  type: "custom",
359
- requestedBy: `phone-desk:${policy.mode}:${policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
401
+ requestedBy: `phone-desk:${policy.mode}:${policy.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? "unknown"}`,
360
402
  payload: {
361
403
  category: "telephony_outbound_call",
404
+ kind: "telephony_outbound_call",
405
+ title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,
406
+ approvalIdentity: policy.approvalIdentity,
407
+ neverAutoApprove: true,
362
408
  mode: policy.mode,
363
409
  workflowId: policy.workflowId,
364
410
  toNumberRedacted: policy.toNumberRedacted,
@@ -421,24 +467,34 @@ function createTelephonyRouter(deps = {}) {
421
467
  }
422
468
  campaignReservationId = reserveCampaignCall();
423
469
  }
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
470
  let result;
441
471
  try {
472
+ const client = createClient(config.telephony);
473
+ if (policy.requiresApproval) {
474
+ const consumed = !!approvalActionId && consumeApproval(approvalActionId);
475
+ if (!consumed) {
476
+ res.status(409).json({
477
+ ok: false,
478
+ error: "telephony_approval_already_used",
479
+ message: "The supplied approval request was already used.",
480
+ call: responseCall
481
+ });
482
+ return;
483
+ }
484
+ writeReceipt({
485
+ kind: "approval_decision",
486
+ status: "ok",
487
+ parent_action_id: approvalActionId,
488
+ summary: `Dograh outbound call approved: ${policy.toNumberRedacted}`,
489
+ meta: {
490
+ mode: policy.mode,
491
+ workflowId: policy.workflowId,
492
+ toNumberRedacted: policy.toNumberRedacted,
493
+ toNumberHash: policy.toNumberHash,
494
+ note: approvalNote
495
+ }
496
+ });
497
+ }
442
498
  result = await client.initiateOutboundCall({
443
499
  workflowId: policy.workflowId,
444
500
  phoneNumber: policy.toNumber,
@@ -1 +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":[]}
1
+ {"version":3,"sources":["../../../src/gateway/routes/telephony.ts"],"sourcesContent":["import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';\nimport { homedir } from 'os';\nimport { dirname, join } from 'path';\nimport { 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 {\n hashPhoneForApproval,\n normalizePhoneNumber,\n redactPhoneNumber,\n validateOutboundCallRequest,\n} 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 consumeApproval?: (approvalActionId: string) => boolean;\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 telephonyStateHome(): string {\n const envHome = process.env.TITAN_HOME?.trim();\n if (envHome) {\n if (envHome === '~') return homedir();\n if (envHome.startsWith('~/')) return join(homedir(), envHome.slice(2));\n return envHome;\n }\n return join(homedir(), '.titan');\n}\n\nfunction consumedApprovalsPath(): string {\n return join(telephonyStateHome(), 'telephony-consumed-approvals.json');\n}\n\nfunction readConsumedApprovalIds(): Set<string> {\n const path = consumedApprovalsPath();\n if (!existsSync(path)) return new Set();\n const parsed = JSON.parse(readFileSync(path, 'utf8')) as unknown;\n if (!Array.isArray(parsed)) return new Set();\n return new Set(parsed.filter((value): value is string => typeof value === 'string'));\n}\n\nfunction isTelephonyApprovalConsumed(approvalActionId: string): boolean {\n if (readConsumedApprovalIds().has(approvalActionId)) return true;\n return readReceipts({ kind: 'approval_decision', limit: 1000 }).some((receipt) => receipt.parent_action_id === approvalActionId && receipt.status === 'ok');\n}\n\nfunction consumeTelephonyApproval(approvalActionId: string): boolean {\n const path = consumedApprovalsPath();\n const consumed = readConsumedApprovalIds();\n if (consumed.has(approvalActionId)) return false;\n consumed.add(approvalActionId);\n mkdirSync(dirname(path), { recursive: true });\n writeFileSync(path, JSON.stringify([...consumed].sort(), null, 2), 'utf8');\n return true;\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 = isTelephonyApprovalConsumed(approvalActionId);\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 approvalIdentity: policy.approvalIdentity,\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 const consumeApproval = deps.consumeApproval ?? consumeTelephonyApproval;\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.approvalIdentity?.slice(0, 16) ?? policy.toNumberHash?.slice(0, 12) ?? 'unknown'}`,\n payload: {\n category: 'telephony_outbound_call',\n kind: 'telephony_outbound_call',\n title: policy.approvalIdentity ?? `${policy.mode}:${policy.workflowId}:${policy.toNumberHash}:${policy.purpose}`,\n approvalIdentity: policy.approvalIdentity,\n neverAutoApprove: true,\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 let result: DograhOutboundCallResult;\n try {\n const client = createClient(config.telephony);\n if (policy.requiresApproval) {\n const consumed = !!approvalActionId && consumeApproval(approvalActionId);\n if (!consumed) {\n res.status(409).json({\n ok: false,\n error: 'telephony_approval_already_used',\n message: 'The supplied approval request was already used.',\n call: responseCall,\n });\n return;\n }\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 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":[]}
@@ -0,0 +1,100 @@
1
+ #!/usr/bin/env node
2
+ import { exec as nodeExec } from "child_process";
3
+ import nodeFs from "fs";
4
+ import { join } from "path";
5
+ import { Router } from "express";
6
+ import logger from "../../utils/logger.js";
7
+ import { getUpdateInfo as defaultGetUpdateInfo } from "../../utils/updater.js";
8
+ const COMPONENT = "update-route";
9
+ const RESTART_DELAY_MS = 1e3;
10
+ const EXPECTED_DOWN_MS = 2500;
11
+ function detectUpdateMode(cwd, existsSyncFn) {
12
+ if (existsSyncFn(join(cwd, ".git"))) return "dev-git";
13
+ if (existsSyncFn("/run/systemd/system") || existsSyncFn(join(cwd, ".systemd-service"))) {
14
+ return "systemd";
15
+ }
16
+ return "npm-global";
17
+ }
18
+ function createUpdateRouter(deps = {}) {
19
+ const router = Router();
20
+ const killSelf = deps.killSelf ?? (() => process.kill(process.pid, "SIGTERM"));
21
+ const existsSyncFn = deps.existsSyncFn ?? nodeFs.existsSync;
22
+ const execFn = deps.execFn ?? nodeExec;
23
+ const getUpdateInfoFn = deps.getUpdateInfoFn ?? defaultGetUpdateInfo;
24
+ const cwdFn = deps.cwdFn ?? (() => process.cwd());
25
+ const envFn = deps.envFn ?? ((key) => process.env[key]);
26
+ router.get("/update", async (_req, res) => {
27
+ const info = await getUpdateInfoFn();
28
+ const mode = detectUpdateMode(cwdFn(), existsSyncFn);
29
+ res.json({ ...info, mode });
30
+ });
31
+ router.post("/update", (req, res) => {
32
+ const restart = req.body?.restart === true;
33
+ const mode = detectUpdateMode(cwdFn(), existsSyncFn);
34
+ const serviceName = envFn("TITAN_SERVICE_NAME") || "titan";
35
+ let command = null;
36
+ if (mode === "dev-git") command = "git pull && npm run build";
37
+ else if (mode === "npm-global") command = "npm update -g titan-agent";
38
+ logger.info(
39
+ COMPONENT,
40
+ `Update requested: mode=${mode} restart=${restart} service=${serviceName} command=${command ?? "(none, restart-only)"}`
41
+ );
42
+ const scheduleRestart = () => {
43
+ setTimeout(() => {
44
+ logger.info(COMPONENT, `Sending SIGTERM to self (pid=${process.pid}) for restart`);
45
+ try {
46
+ killSelf();
47
+ } catch (err) {
48
+ logger.error(COMPONENT, `SIGTERM failed: ${err.message}`);
49
+ }
50
+ }, RESTART_DELAY_MS);
51
+ };
52
+ if (command === null) {
53
+ const result = {
54
+ ok: true,
55
+ mode,
56
+ willRestart: restart,
57
+ expectedDownMs: restart ? EXPECTED_DOWN_MS : void 0,
58
+ message: restart ? `Restarting ${serviceName}. systemd will revive the process on the on-disk files.` : 'No-op: no command to run for systemd mode. Pass {"restart": true} to reload the process.'
59
+ };
60
+ res.json(result);
61
+ if (restart) scheduleRestart();
62
+ return;
63
+ }
64
+ execFn(command, { timeout: 18e4 }, (error, stdout) => {
65
+ if (error) {
66
+ logger.error(COMPONENT, `Update command failed (${mode}): ${error.message}`);
67
+ if (!res.headersSent) {
68
+ const result = {
69
+ ok: false,
70
+ mode,
71
+ willRestart: false,
72
+ error: error.message,
73
+ output: stdout?.slice(-500)
74
+ };
75
+ res.json(result);
76
+ }
77
+ return;
78
+ }
79
+ logger.info(COMPONENT, `Update command succeeded (${mode})`);
80
+ if (!res.headersSent) {
81
+ const result = {
82
+ ok: true,
83
+ mode,
84
+ willRestart: restart,
85
+ expectedDownMs: restart ? EXPECTED_DOWN_MS : void 0,
86
+ message: restart ? "Update complete. Restarting." : "Update complete.",
87
+ output: stdout?.slice(-500)
88
+ };
89
+ res.json(result);
90
+ }
91
+ if (restart) scheduleRestart();
92
+ });
93
+ });
94
+ return router;
95
+ }
96
+ export {
97
+ createUpdateRouter,
98
+ detectUpdateMode
99
+ };
100
+ //# sourceMappingURL=update.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../src/gateway/routes/update.ts"],"sourcesContent":["/**\n * Tests live in tests/gateway/updateRoute.test.ts. Keep DI seams intact so\n * those tests can mock killSelf/existsSyncFn/execFn/getUpdateInfoFn without\n * touching real disk, real npm, or real process signals.\n *\n * /api/update route — extracted from server.ts during the update-button fix.\n *\n * Why this exists as its own module:\n * The inline POST /api/update handler in server.ts had four shipping bugs\n * that broke the in-app \"Update to vX.Y.Z\" button for systemd installs\n * (which is how Tony's box and the docs both deploy TITAN):\n *\n * 1. Hard-coded `sudo systemctl restart titan-gateway` — the real\n * service name is `titan` everywhere we ship.\n * 2. Required passwordless sudo from the service user (`dj`), which\n * is not configured by default → the detached restart script hung\n * on the password prompt forever and the service stayed dead.\n * 3. Assumed `/opt/TITAN` is a git working tree and ran `git pull` in\n * it. `/opt/TITAN` is an rsync target per the deploy procedure, not\n * a git checkout, so this either errored or no-op'd silently.\n * 4. Returned `{ok: true}` to the UI before the restart actually ran,\n * so users saw \"Update completed\" while the service was dying.\n *\n * The fix:\n * - For systemd installs: send SIGTERM to ourselves and let\n * `Restart=always` revive the process on whatever files are on disk.\n * This is exactly the manual recovery path we already use; the button\n * just automates it. No sudo, no service-name guessing, no fragile\n * detached scripts.\n * - For git-checkout dev installs: still run `git pull && npm run build`\n * first, then SIGTERM-to-self (if Restart=always is wired) or spawn\n * a fresh node child (if not).\n * - For global-npm installs (no systemd, no .git): keep the existing\n * `npm update -g titan-agent` path — works for users on nvm/volta/fnm.\n * Users who installed with `sudo npm i -g` will still need to update\n * from a terminal; that's outside the scope of an in-app button.\n * - Always return an honest `{ok, willRestart, expectedDownMs, mode}`\n * shape so the UI can show a real \"Reconnecting…\" state instead of\n * a fake \"Update completed\" toast.\n */\nimport { exec as nodeExec, type ExecException } from 'child_process';\nimport nodeFs from 'fs';\nimport { join } from 'path';\nimport { Router } from 'express';\nimport logger from '../../utils/logger.js';\nimport { getUpdateInfo as defaultGetUpdateInfo } from '../../utils/updater.js';\n\nconst COMPONENT = 'update-route';\n\nexport type UpdateMode = 'dev-git' | 'systemd' | 'npm-global';\n\n/**\n * Returned from POST /api/update. Older clients that only check `ok` keep\n * working; newer clients (StatusBar, TitanCanvas) can read `willRestart`\n * and `expectedDownMs` to drive a reconnect UX.\n */\nexport interface UpdateActionResult {\n ok: boolean;\n mode: UpdateMode;\n willRestart: boolean;\n expectedDownMs?: number;\n message?: string;\n output?: string;\n error?: string;\n}\n\nexport interface UpdateRouterDeps {\n /** Override SIGTERM target for tests. Default: send SIGTERM to current pid. */\n killSelf?: () => void;\n /** fs.existsSync override for tests. */\n existsSyncFn?: (path: string) => boolean;\n /** child_process.exec override for tests. */\n execFn?: typeof nodeExec;\n /** getUpdateInfo override for tests. */\n getUpdateInfoFn?: typeof defaultGetUpdateInfo;\n /** Override process.cwd() for tests. */\n cwdFn?: () => string;\n /** Override env lookup for tests. */\n envFn?: (key: string) => string | undefined;\n}\n\nconst RESTART_DELAY_MS = 1000;\nconst EXPECTED_DOWN_MS = 2500; // Conservative: systemd Restart=always usually revives within ~1s.\n\n/**\n * Decide which update path applies based on what's on disk.\n *\n * Order matters:\n * 1. `.git/` at cwd → developer checkout, do the full git-pull+build path.\n * We check this first because a dev who happens to be on a systemd box\n * (rare but real) should still get build behavior, not a blind restart.\n * 2. `/run/systemd/system` exists or `.systemd-service` marker at cwd →\n * production systemd install. Restart-only path; operator handles\n * file deployment via rsync/ansible/docker.\n * 3. Otherwise → global npm install. `npm update -g`.\n */\nexport function detectUpdateMode(\n cwd: string,\n existsSyncFn: (p: string) => boolean,\n): UpdateMode {\n if (existsSyncFn(join(cwd, '.git'))) return 'dev-git';\n if (existsSyncFn('/run/systemd/system') || existsSyncFn(join(cwd, '.systemd-service'))) {\n return 'systemd';\n }\n return 'npm-global';\n}\n\nexport function createUpdateRouter(deps: UpdateRouterDeps = {}): Router {\n const router = Router();\n const killSelf = deps.killSelf ?? (() => process.kill(process.pid, 'SIGTERM'));\n const existsSyncFn = deps.existsSyncFn ?? nodeFs.existsSync;\n const execFn = deps.execFn ?? nodeExec;\n const getUpdateInfoFn = deps.getUpdateInfoFn ?? defaultGetUpdateInfo;\n const cwdFn = deps.cwdFn ?? (() => process.cwd());\n const envFn = deps.envFn ?? ((key: string) => process.env[key]);\n\n router.get('/update', async (_req, res) => {\n const info = await getUpdateInfoFn();\n const mode = detectUpdateMode(cwdFn(), existsSyncFn);\n res.json({ ...info, mode });\n });\n\n router.post('/update', (req, res) => {\n const restart = req.body?.restart === true;\n const mode = detectUpdateMode(cwdFn(), existsSyncFn);\n const serviceName = envFn('TITAN_SERVICE_NAME') || 'titan';\n\n // Commands per mode.\n // Systemd: nothing to fetch — files are deployed externally (rsync/ansible/docker).\n // The button's only job is to reload the running process into them.\n // Dev-git: pull + build, then restart so the new dist/ is loaded.\n // npm-global: shell out to npm; if the user is on nvm/volta/fnm this works\n // without sudo. If they did `sudo npm i -g`, npm will EACCES and we\n // surface the error honestly instead of pretending.\n let command: string | null = null;\n if (mode === 'dev-git') command = 'git pull && npm run build';\n else if (mode === 'npm-global') command = 'npm update -g titan-agent';\n // mode === 'systemd' → command stays null, we skip exec entirely\n\n logger.info(\n COMPONENT,\n `Update requested: mode=${mode} restart=${restart} service=${serviceName} command=${command ?? '(none, restart-only)'}`,\n );\n\n const scheduleRestart = (): void => {\n // setTimeout-then-SIGTERM. With systemd Restart=always (verified on Titan PC)\n // this gives the HTTP response time to flush before we tear the process down.\n setTimeout(() => {\n logger.info(COMPONENT, `Sending SIGTERM to self (pid=${process.pid}) for restart`);\n try {\n killSelf();\n } catch (err) {\n logger.error(COMPONENT, `SIGTERM failed: ${(err as Error).message}`);\n }\n }, RESTART_DELAY_MS);\n };\n\n if (command === null) {\n // Pure-restart path. systemd will revive us.\n const result: UpdateActionResult = {\n ok: true,\n mode,\n willRestart: restart,\n expectedDownMs: restart ? EXPECTED_DOWN_MS : undefined,\n message: restart\n ? `Restarting ${serviceName}. systemd will revive the process on the on-disk files.`\n : 'No-op: no command to run for systemd mode. Pass {\"restart\": true} to reload the process.',\n };\n res.json(result);\n if (restart) scheduleRestart();\n return;\n }\n\n // dev-git or npm-global path: run command, then optionally restart.\n execFn(command, { timeout: 180_000 }, (error: ExecException | null, stdout: string) => {\n if (error) {\n logger.error(COMPONENT, `Update command failed (${mode}): ${error.message}`);\n if (!res.headersSent) {\n const result: UpdateActionResult = {\n ok: false,\n mode,\n willRestart: false,\n error: error.message,\n output: stdout?.slice(-500),\n };\n res.json(result);\n }\n return;\n }\n logger.info(COMPONENT, `Update command succeeded (${mode})`);\n if (!res.headersSent) {\n const result: UpdateActionResult = {\n ok: true,\n mode,\n willRestart: restart,\n expectedDownMs: restart ? EXPECTED_DOWN_MS : undefined,\n message: restart ? 'Update complete. Restarting.' : 'Update complete.',\n output: stdout?.slice(-500),\n };\n res.json(result);\n }\n if (restart) scheduleRestart();\n });\n });\n\n return router;\n}\n"],"mappings":"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","names":[]}
@@ -9,7 +9,6 @@ import { fileURLToPath } from "url";
9
9
  import { homedir, hostname as osHostname, cpus, loadavg, totalmem, freemem } from "os";
10
10
  import { statfs } from "fs/promises";
11
11
  import { randomBytes, timingSafeEqual } from "crypto";
12
- import { exec, spawn } from "child_process";
13
12
  import fs from "fs";
14
13
  import { loadConfig, updateConfig } from "../config/config.js";
15
14
  import { loadProfile, saveProfile } from "../memory/relationship.js";
@@ -42,7 +41,6 @@ import logger, { initFileLogger } from "../utils/logger.js";
42
41
  import { TITAN_VERSION, TITAN_NAME, TITAN_LOGS_DIR, TITAN_HOME } from "../utils/constants.js";
43
42
  import { getRestartStats as getRestartStatsSync } from "../utils/restartTracker.js";
44
43
  import { recordStartupAnalytics, startHeartbeatAnalytics } from "../analytics/collector.js";
45
- import { getUpdateInfo } from "../utils/updater.js";
46
44
  import { getMissionControlHTML } from "./dashboard.js";
47
45
  import { serializePrometheus, titanRequestsTotal, titanRequestDuration, titanErrorsTotal, titanActiveSessions, titanToolCallsTotal, titanTokensTotal, titanModelRequestsTotal } from "./metrics.js";
48
46
  import { createHardwareRouter } from "./routes/hardwareRouter.js";
@@ -88,6 +86,7 @@ import { createTestsRouter } from "./routes/tests.js";
88
86
  import { createSystemRouter } from "./routes/systemRouter.js";
89
87
  import { createVoiceRouter } from "./routes/voiceRouter.js";
90
88
  import { createTelephonyRouter } from "./routes/telephony.js";
89
+ import { createUpdateRouter } from "./routes/update.js";
91
90
  import { createSocialRouter } from "./routes/socialRouter.js";
92
91
  import { createWatchRouter } from "./routes/watchRouter.js";
93
92
  import { setupSSEFlush } from "../utils/sseFlush.js";
@@ -2567,69 +2566,7 @@ data: ${JSON.stringify(structured)}
2567
2566
  // Last 20 for detail view
2568
2567
  });
2569
2568
  });
2570
- app.get("/api/update", async (_req, res) => {
2571
- const info = await getUpdateInfo();
2572
- res.json(info);
2573
- });
2574
- app.post("/api/update", (req, res) => {
2575
- const isLocalDev = fs.existsSync(join(process.cwd(), ".git"));
2576
- const isSystemd = fs.existsSync("/run/systemd/system") || fs.existsSync(join(process.cwd(), ".systemd-service"));
2577
- const restart = req.body?.restart === true;
2578
- let command;
2579
- let postCommand = null;
2580
- if (isLocalDev) {
2581
- command = "git pull && npm run build";
2582
- if (restart) {
2583
- postCommand = "restart";
2584
- }
2585
- } else if (isSystemd) {
2586
- command = "git pull && npm run build";
2587
- if (restart) {
2588
- postCommand = "systemctl";
2589
- }
2590
- } else {
2591
- command = "npm update -g titan-agent";
2592
- }
2593
- logger.info(COMPONENT, `Triggering update: ${command} (isDev=${isLocalDev}, isSystemd=${isSystemd}, restart=${restart})`);
2594
- exec(command, { timeout: 18e4 }, (error, stdout, _stderr) => {
2595
- if (error) {
2596
- logger.error(COMPONENT, `Update failed: ${error.message}`);
2597
- if (!res.headersSent) res.json({ ok: false, error: error.message });
2598
- return;
2599
- }
2600
- logger.info(COMPONENT, `Update completed successfully.\\n${stdout}`);
2601
- if (!res.headersSent) {
2602
- res.json({ ok: true, message: "Update completed", restarting: restart, output: stdout.slice(-500) });
2603
- }
2604
- if (restart) {
2605
- logger.info(COMPONENT, "Scheduling restart in 2 seconds...");
2606
- const cwd = process.cwd();
2607
- if (postCommand === "systemctl") {
2608
- const scriptPath = "/tmp/titan-restart.sh";
2609
- fs.writeFileSync(scriptPath, [
2610
- "#!/bin/bash",
2611
- "sleep 2",
2612
- `cd "${cwd}"`,
2613
- "sudo systemctl restart titan-gateway"
2614
- ].join("\n"), { mode: 493 });
2615
- spawn("bash", [scriptPath], { detached: true, stdio: "ignore" }).unref();
2616
- } else {
2617
- const scriptPath = "/tmp/titan-restart.sh";
2618
- fs.writeFileSync(scriptPath, [
2619
- "#!/bin/bash",
2620
- "sleep 2",
2621
- `cd "${cwd}"`,
2622
- "nohup node dist/cli/index.js gateway >> /tmp/titan-gateway.log 2>&1 &"
2623
- ].join("\n"), { mode: 493 });
2624
- spawn("bash", [scriptPath], { detached: true, stdio: "ignore" }).unref();
2625
- }
2626
- setTimeout(() => {
2627
- logger.info(COMPONENT, "Exiting for restart...");
2628
- process.exit(0);
2629
- }, 1e3);
2630
- }
2631
- });
2632
- });
2569
+ app.use("/api", createUpdateRouter());
2633
2570
  app.get("/api/config", (_req, res) => {
2634
2571
  const cfg = loadConfig();
2635
2572
  res.json({