@hotmeshio/long-tail 0.10.8 → 0.10.10

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 (296) hide show
  1. package/build/api/escalations/bulk.js +7 -1
  2. package/build/api/escalations/helpers.d.ts +10 -1
  3. package/build/api/escalations/helpers.js +52 -12
  4. package/build/api/index.d.ts +1 -0
  5. package/build/api/index.js +2 -1
  6. package/build/api/personas.d.ts +121 -0
  7. package/build/api/personas.js +341 -0
  8. package/build/bin/ltc.js +23 -0
  9. package/build/lib/cli/commands/personas.d.ts +16 -0
  10. package/build/lib/cli/commands/personas.js +42 -0
  11. package/build/lib/db/schemas/024_personas.sql +53 -0
  12. package/build/lib/events/matching.d.ts +10 -0
  13. package/build/lib/events/matching.js +14 -0
  14. package/build/lib/events/publish.d.ts +8 -2
  15. package/build/lib/events/publish.js +9 -2
  16. package/build/lib/events/socketio.d.ts +8 -2
  17. package/build/lib/events/socketio.js +36 -3
  18. package/build/lib/events/system-events.js +9 -1
  19. package/build/routes/index.js +2 -0
  20. package/build/routes/personas.d.ts +2 -0
  21. package/build/routes/personas.js +118 -0
  22. package/build/routes/users.js +35 -0
  23. package/build/sdk/index.d.ts +15 -0
  24. package/build/sdk/index.js +16 -0
  25. package/build/services/escalation/crud.js +40 -2
  26. package/build/services/orchestrator/condition.d.ts +1 -1
  27. package/build/services/orchestrator/condition.js +1 -1
  28. package/build/services/persona/index.d.ts +92 -0
  29. package/build/services/persona/index.js +217 -0
  30. package/build/services/persona/sql.d.ts +24 -0
  31. package/build/services/persona/sql.js +158 -0
  32. package/build/services/persona/types.d.ts +23 -0
  33. package/build/services/persona/types.js +33 -0
  34. package/build/services/topics/system-topics.d.ts +0 -4
  35. package/build/services/topics/system-topics.js +57 -14
  36. package/build/services/user/sql.d.ts +8 -5
  37. package/build/services/user/sql.js +15 -5
  38. package/build/system/mcp-servers/admin/index.d.ts +1 -0
  39. package/build/system/mcp-servers/admin/index.js +5 -2
  40. package/build/system/mcp-servers/admin/personas.d.ts +8 -0
  41. package/build/system/mcp-servers/admin/personas.js +186 -0
  42. package/build/system/mcp-servers/admin/schemas.d.ts +91 -0
  43. package/build/system/mcp-servers/admin/schemas.js +42 -2
  44. package/build/system/seed/tool-manifests-admin.d.ts +975 -0
  45. package/build/system/seed/tool-manifests-admin.js +11 -0
  46. package/build/tsconfig.tsbuildinfo +1 -1
  47. package/build/types/events.d.ts +11 -3
  48. package/build/types/index.d.ts +1 -0
  49. package/build/types/persona.d.ts +56 -0
  50. package/build/types/persona.js +2 -0
  51. package/build/types/user.d.ts +2 -0
  52. package/dashboard/dist/assets/{AdminDashboard-D0HdEkT9.js → AdminDashboard-cGYuppUP.js} +2 -2
  53. package/dashboard/dist/assets/{AdminDashboard-D0HdEkT9.js.map → AdminDashboard-cGYuppUP.js.map} +1 -1
  54. package/dashboard/dist/assets/{AgentConfigPage-CtURUMOV.js → AgentConfigPage-DwaUyoCy.js} +2 -2
  55. package/dashboard/dist/assets/{AgentConfigPage-CtURUMOV.js.map → AgentConfigPage-DwaUyoCy.js.map} +1 -1
  56. package/dashboard/dist/assets/AgentDetailPage-DxpFaltM.js +4 -0
  57. package/dashboard/dist/assets/AgentDetailPage-DxpFaltM.js.map +1 -0
  58. package/dashboard/dist/assets/{AgentsPage-DBequneI.js → AgentsPage-ByOoA8oT.js} +2 -2
  59. package/dashboard/dist/assets/{AgentsPage-DBequneI.js.map → AgentsPage-ByOoA8oT.js.map} +1 -1
  60. package/dashboard/dist/assets/AutoGrowTextarea-y-oNY51_.js +2 -0
  61. package/dashboard/dist/assets/AutoGrowTextarea-y-oNY51_.js.map +1 -0
  62. package/dashboard/dist/assets/{AvailableEscalationsPage-Bk7fFV2e.js → AvailableEscalationsPage-C1vtTvqP.js} +2 -2
  63. package/dashboard/dist/assets/AvailableEscalationsPage-C1vtTvqP.js.map +1 -0
  64. package/dashboard/dist/assets/{BotPicker-BMXokkH7.js → BotPicker-Dohv8dhR.js} +2 -2
  65. package/dashboard/dist/assets/{BotPicker-BMXokkH7.js.map → BotPicker-Dohv8dhR.js.map} +1 -1
  66. package/dashboard/dist/assets/{CapabilitiesPage-w-D6xraE.js → CapabilitiesPage-DeijFG3J.js} +2 -2
  67. package/dashboard/dist/assets/{CapabilitiesPage-w-D6xraE.js.map → CapabilitiesPage-DeijFG3J.js.map} +1 -1
  68. package/dashboard/dist/assets/{CollapsibleSection-IPL1H1bQ.js → CollapsibleSection-BPNwS4eO.js} +2 -2
  69. package/dashboard/dist/assets/{CollapsibleSection-IPL1H1bQ.js.map → CollapsibleSection-BPNwS4eO.js.map} +1 -1
  70. package/dashboard/dist/assets/CountdownTimer-BF71ldGF.js +2 -0
  71. package/dashboard/dist/assets/CountdownTimer-BF71ldGF.js.map +1 -0
  72. package/dashboard/dist/assets/{CredentialsPage-DR48hDES.js → CredentialsPage-oeNmMzTM.js} +2 -2
  73. package/dashboard/dist/assets/{CredentialsPage-DR48hDES.js.map → CredentialsPage-oeNmMzTM.js.map} +1 -1
  74. package/dashboard/dist/assets/{CronLabel-KQEj0cDS.js → CronLabel-CebR1m2k.js} +2 -2
  75. package/dashboard/dist/assets/{CronLabel-KQEj0cDS.js.map → CronLabel-CebR1m2k.js.map} +1 -1
  76. package/dashboard/dist/assets/CustomDurationPicker-Bh9QdBVw.js +2 -0
  77. package/dashboard/dist/assets/CustomDurationPicker-Bh9QdBVw.js.map +1 -0
  78. package/dashboard/dist/assets/{DropZone-BrnAkQxF.js → DropZone-Bavzt2QR.js} +2 -2
  79. package/dashboard/dist/assets/{DropZone-BrnAkQxF.js.map → DropZone-Bavzt2QR.js.map} +1 -1
  80. package/dashboard/dist/assets/ElapsedCell-Caa6CU_y.js +2 -0
  81. package/dashboard/dist/assets/{ElapsedCell-Cflz7Pb5.js.map → ElapsedCell-Caa6CU_y.js.map} +1 -1
  82. package/dashboard/dist/assets/{EscalationListSchemaPage-BTBZ8OBY.js → EscalationListSchemaPage-CAGfq0eK.js} +3 -3
  83. package/dashboard/dist/assets/{EscalationListSchemaPage-BTBZ8OBY.js.map → EscalationListSchemaPage-CAGfq0eK.js.map} +1 -1
  84. package/dashboard/dist/assets/{EscalationSchemaPage-Cd9fR2EA.js → EscalationSchemaPage-qDD4ur6P.js} +2 -2
  85. package/dashboard/dist/assets/{EscalationSchemaPage-Cd9fR2EA.js.map → EscalationSchemaPage-qDD4ur6P.js.map} +1 -1
  86. package/dashboard/dist/assets/{EscalationsOverview-BzNFcA0i.js → EscalationsOverview-CC1wRTzy.js} +2 -2
  87. package/dashboard/dist/assets/{EscalationsOverview-BzNFcA0i.js.map → EscalationsOverview-CC1wRTzy.js.map} +1 -1
  88. package/dashboard/dist/assets/{EventTable-S2BJqOt2.js → EventTable-4JeTcXr_.js} +2 -2
  89. package/dashboard/dist/assets/{EventTable-S2BJqOt2.js.map → EventTable-4JeTcXr_.js.map} +1 -1
  90. package/dashboard/dist/assets/{FilterBar-CeXwNVn6.js → FilterBar-BOV8DWIO.js} +2 -2
  91. package/dashboard/dist/assets/{FilterBar-CeXwNVn6.js.map → FilterBar-BOV8DWIO.js.map} +1 -1
  92. package/dashboard/dist/assets/{GraphInvokePage-CBjXjOLX.js → GraphInvokePage-C0i1speW.js} +2 -2
  93. package/dashboard/dist/assets/{GraphInvokePage-CBjXjOLX.js.map → GraphInvokePage-C0i1speW.js.map} +1 -1
  94. package/dashboard/dist/assets/HomePage-HPCd8SjF.js +2 -0
  95. package/dashboard/dist/assets/HomePage-HPCd8SjF.js.map +1 -0
  96. package/dashboard/dist/assets/{ListToolbar-Dx6DuFdR.js → ListToolbar-BFHVOba6.js} +2 -2
  97. package/dashboard/dist/assets/{ListToolbar-Dx6DuFdR.js.map → ListToolbar-BFHVOba6.js.map} +1 -1
  98. package/dashboard/dist/assets/{McpOverview-Cj8_UyWJ.js → McpOverview-Chn2zIXr.js} +2 -2
  99. package/dashboard/dist/assets/{McpOverview-Cj8_UyWJ.js.map → McpOverview-Chn2zIXr.js.map} +1 -1
  100. package/dashboard/dist/assets/McpQueryDetailPage-Cv1FKrvw.js +5 -0
  101. package/dashboard/dist/assets/McpQueryDetailPage-Cv1FKrvw.js.map +1 -0
  102. package/dashboard/dist/assets/McpQueryPage-Dzn8JE4E.js +2 -0
  103. package/dashboard/dist/assets/McpQueryPage-Dzn8JE4E.js.map +1 -0
  104. package/dashboard/dist/assets/{McpRunDetailPage-BqhRwkGk.js → McpRunDetailPage-CuNtR4Ed.js} +2 -2
  105. package/dashboard/dist/assets/{McpRunDetailPage-BqhRwkGk.js.map → McpRunDetailPage-CuNtR4Ed.js.map} +1 -1
  106. package/dashboard/dist/assets/{McpRunsPage-CNXYU4iX.js → McpRunsPage-C8iifeKN.js} +2 -2
  107. package/dashboard/dist/assets/{McpRunsPage-CNXYU4iX.js.map → McpRunsPage-C8iifeKN.js.map} +1 -1
  108. package/dashboard/dist/assets/{NamespacePill-DQGzN0Ph.js → NamespacePill-ep9Zmokr.js} +2 -2
  109. package/dashboard/dist/assets/{NamespacePill-DQGzN0Ph.js.map → NamespacePill-ep9Zmokr.js.map} +1 -1
  110. package/dashboard/dist/assets/{OperationsPage-C7UB_d-X.js → OperationsPage-flrZ-EJ9.js} +2 -2
  111. package/dashboard/dist/assets/{OperationsPage-C7UB_d-X.js.map → OperationsPage-flrZ-EJ9.js.map} +1 -1
  112. package/dashboard/dist/assets/OperatorDashboard-Dhs2eo8p.js +2 -0
  113. package/dashboard/dist/assets/OperatorDashboard-Dhs2eo8p.js.map +1 -0
  114. package/dashboard/dist/assets/{PageHeader-DlcE3U5L.js → PageHeader-Bw8RNkqR.js} +2 -2
  115. package/dashboard/dist/assets/{PageHeader-DlcE3U5L.js.map → PageHeader-Bw8RNkqR.js.map} +1 -1
  116. package/dashboard/dist/assets/{PageHeaderWithStats-BMhnR3-9.js → PageHeaderWithStats-NR_4idn6.js} +2 -2
  117. package/dashboard/dist/assets/{PageHeaderWithStats-BMhnR3-9.js.map → PageHeaderWithStats-NR_4idn6.js.map} +1 -1
  118. package/dashboard/dist/assets/PersonaDetailPage-DKILoUqL.js +2 -0
  119. package/dashboard/dist/assets/PersonaDetailPage-DKILoUqL.js.map +1 -0
  120. package/dashboard/dist/assets/PersonasPage-qvzHTrFw.js +2 -0
  121. package/dashboard/dist/assets/PersonasPage-qvzHTrFw.js.map +1 -0
  122. package/dashboard/dist/assets/ProcessDetailPage-C3LfeKvi.js +2 -0
  123. package/dashboard/dist/assets/ProcessDetailPage-C3LfeKvi.js.map +1 -0
  124. package/dashboard/dist/assets/{ProcessesListPage-DatKHVkd.js → ProcessesListPage-OoZ_u4ty.js} +2 -2
  125. package/dashboard/dist/assets/{ProcessesListPage-DatKHVkd.js.map → ProcessesListPage-OoZ_u4ty.js.map} +1 -1
  126. package/dashboard/dist/assets/RoleDetailPage-D70jTLZm.js +8 -0
  127. package/dashboard/dist/assets/RoleDetailPage-D70jTLZm.js.map +1 -0
  128. package/dashboard/dist/assets/{RolePill-dzo14hB_.js → RolePill-TySZq7dB.js} +2 -2
  129. package/dashboard/dist/assets/{RolePill-dzo14hB_.js.map → RolePill-TySZq7dB.js.map} +1 -1
  130. package/dashboard/dist/assets/RolesPage-CNueMVuJ.js +2 -0
  131. package/dashboard/dist/assets/RolesPage-CNueMVuJ.js.map +1 -0
  132. package/dashboard/dist/assets/{RunAsSelector-VbBn0o7s.js → RunAsSelector-CwWEpiSF.js} +2 -2
  133. package/dashboard/dist/assets/{RunAsSelector-VbBn0o7s.js.map → RunAsSelector-CwWEpiSF.js.map} +1 -1
  134. package/dashboard/dist/assets/ScopeBadge-C7Uixtmk.js +2 -0
  135. package/dashboard/dist/assets/ScopeBadge-C7Uixtmk.js.map +1 -0
  136. package/dashboard/dist/assets/StatCard-Ca-ngCaD.js +2 -0
  137. package/dashboard/dist/assets/StatCard-Ca-ngCaD.js.map +1 -0
  138. package/dashboard/dist/assets/{StatusBadge-BbqghFMi.js → StatusBadge-BwUvykDo.js} +2 -2
  139. package/dashboard/dist/assets/{StatusBadge-BbqghFMi.js.map → StatusBadge-BwUvykDo.js.map} +1 -1
  140. package/dashboard/dist/assets/{StickyPagination-BI8tDf2V.js → StickyPagination-DvIF3kQK.js} +2 -2
  141. package/dashboard/dist/assets/{StickyPagination-BI8tDf2V.js.map → StickyPagination-DvIF3kQK.js.map} +1 -1
  142. package/dashboard/dist/assets/{StreamMessageDetail-BObbEz7P.js → StreamMessageDetail-DkFK_XGD.js} +2 -2
  143. package/dashboard/dist/assets/StreamMessageDetail-DkFK_XGD.js.map +1 -0
  144. package/dashboard/dist/assets/{SwimlaneTimeline-BHjAtOGC.js → SwimlaneTimeline-CMw-Et6h.js} +2 -2
  145. package/dashboard/dist/assets/{SwimlaneTimeline-BHjAtOGC.js.map → SwimlaneTimeline-CMw-Et6h.js.map} +1 -1
  146. package/dashboard/dist/assets/{TagInput-cyhTZTwe.js → TagInput-D8gSp48D.js} +2 -2
  147. package/dashboard/dist/assets/{TagInput-cyhTZTwe.js.map → TagInput-D8gSp48D.js.map} +1 -1
  148. package/dashboard/dist/assets/{TaskDetailPage-B-E1dqUw.js → TaskDetailPage-BhXuChOf.js} +2 -2
  149. package/dashboard/dist/assets/{TaskDetailPage-B-E1dqUw.js.map → TaskDetailPage-BhXuChOf.js.map} +1 -1
  150. package/dashboard/dist/assets/{TaskQueuePill-_zIpG4bG.js → TaskQueuePill-CHqolBmU.js} +2 -2
  151. package/dashboard/dist/assets/{TaskQueuePill-_zIpG4bG.js.map → TaskQueuePill-CHqolBmU.js.map} +1 -1
  152. package/dashboard/dist/assets/{TasksListPage-urJ4XiUi.js → TasksListPage-6JHQQ5QZ.js} +2 -2
  153. package/dashboard/dist/assets/{TasksListPage-urJ4XiUi.js.map → TasksListPage-6JHQQ5QZ.js.map} +1 -1
  154. package/dashboard/dist/assets/{TimeAgo-D6Dg0SQs.js → TimeAgo-CInx-vzY.js} +2 -2
  155. package/dashboard/dist/assets/{TimeAgo-D6Dg0SQs.js.map → TimeAgo-CInx-vzY.js.map} +1 -1
  156. package/dashboard/dist/assets/{TimestampCell-DG__Gr-e.js → TimestampCell-Fq17Zrsu.js} +2 -2
  157. package/dashboard/dist/assets/{TimestampCell-DG__Gr-e.js.map → TimestampCell-Fq17Zrsu.js.map} +1 -1
  158. package/dashboard/dist/assets/{ToolPill-CKHYG6eV.js → ToolPill-BbgMDhaE.js} +2 -2
  159. package/dashboard/dist/assets/{ToolPill-CKHYG6eV.js.map → ToolPill-BbgMDhaE.js.map} +1 -1
  160. package/dashboard/dist/assets/{ToolTestPanel-D-hGLf95.js → ToolTestPanel-DhjEe59s.js} +2 -2
  161. package/dashboard/dist/assets/{ToolTestPanel-D-hGLf95.js.map → ToolTestPanel-DhjEe59s.js.map} +1 -1
  162. package/dashboard/dist/assets/{TopicDetailPage-B10i4xV2.js → TopicDetailPage-DbIdmQpQ.js} +3 -3
  163. package/dashboard/dist/assets/{TopicDetailPage-B10i4xV2.js.map → TopicDetailPage-DbIdmQpQ.js.map} +1 -1
  164. package/dashboard/dist/assets/TopicsPage-CAP-GUbr.js +2 -0
  165. package/dashboard/dist/assets/{TopicsPage-C5THflVQ.js.map → TopicsPage-CAP-GUbr.js.map} +1 -1
  166. package/dashboard/dist/assets/{UserName-CZTpouta.js → UserName-DggcLkSc.js} +2 -2
  167. package/dashboard/dist/assets/{UserName-CZTpouta.js.map → UserName-DggcLkSc.js.map} +1 -1
  168. package/dashboard/dist/assets/{WorkflowExecutionPage-mbnj6saI.js → WorkflowExecutionPage-Bmra5fVt.js} +2 -2
  169. package/dashboard/dist/assets/{WorkflowExecutionPage-mbnj6saI.js.map → WorkflowExecutionPage-Bmra5fVt.js.map} +1 -1
  170. package/dashboard/dist/assets/{WorkflowPill-DlSVXnnj.js → WorkflowPill-CcUUVugA.js} +2 -2
  171. package/dashboard/dist/assets/{WorkflowPill-DlSVXnnj.js.map → WorkflowPill-CcUUVugA.js.map} +1 -1
  172. package/dashboard/dist/assets/{WorkflowsDashboard-DC_9GrpY.js → WorkflowsDashboard-BQsZAGYU.js} +2 -2
  173. package/dashboard/dist/assets/{WorkflowsDashboard-DC_9GrpY.js.map → WorkflowsDashboard-BQsZAGYU.js.map} +1 -1
  174. package/dashboard/dist/assets/{WorkflowsOverview-ClSH1UwG.js → WorkflowsOverview-CeliraHZ.js} +2 -2
  175. package/dashboard/dist/assets/{WorkflowsOverview-ClSH1UwG.js.map → WorkflowsOverview-CeliraHZ.js.map} +1 -1
  176. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BkgoB7oD.js → YamlWorkflowDetailPage-DGxlonrV.js} +2 -2
  177. package/dashboard/dist/assets/{YamlWorkflowDetailPage-BkgoB7oD.js.map → YamlWorkflowDetailPage-DGxlonrV.js.map} +1 -1
  178. package/dashboard/dist/assets/{YamlWorkflowsPage-Db_REA2f.js → YamlWorkflowsPage-DnT8UiyQ.js} +2 -2
  179. package/dashboard/dist/assets/{YamlWorkflowsPage-Db_REA2f.js.map → YamlWorkflowsPage-DnT8UiyQ.js.map} +1 -1
  180. package/dashboard/dist/assets/{agents-Dvb8j9AB.js → agents-mRV7m4sh.js} +2 -2
  181. package/dashboard/dist/assets/{agents-Dvb8j9AB.js.map → agents-mRV7m4sh.js.map} +1 -1
  182. package/dashboard/dist/assets/{bots-D9jYHjBM.js → bots-SONCZlCM.js} +2 -2
  183. package/dashboard/dist/assets/{bots-D9jYHjBM.js.map → bots-SONCZlCM.js.map} +1 -1
  184. package/dashboard/dist/assets/{capabilities-BAto9JcQ.js → capabilities-DZjCMN32.js} +2 -2
  185. package/dashboard/dist/assets/{capabilities-BAto9JcQ.js.map → capabilities-DZjCMN32.js.map} +1 -1
  186. package/dashboard/dist/assets/{controlplane-2Aic2Tfm.js → controlplane-RQNXjna2.js} +2 -2
  187. package/dashboard/dist/assets/{controlplane-2Aic2Tfm.js.map → controlplane-RQNXjna2.js.map} +1 -1
  188. package/dashboard/dist/assets/{escalation-columns-DPKPJtw3.js → escalation-columns-XLEcZRCg.js} +2 -2
  189. package/dashboard/dist/assets/{escalation-columns-DPKPJtw3.js.map → escalation-columns-XLEcZRCg.js.map} +1 -1
  190. package/dashboard/dist/assets/index-2BwHxjCw.css +1 -0
  191. package/dashboard/dist/assets/{index-BZbDwh_Y.js → index-C6GsrvX1.js} +2 -2
  192. package/dashboard/dist/assets/{index-BZbDwh_Y.js.map → index-C6GsrvX1.js.map} +1 -1
  193. package/dashboard/dist/assets/{index-BzQIzZfd.js → index-CK-6m8e_.js} +2 -2
  194. package/dashboard/dist/assets/{index-BzQIzZfd.js.map → index-CK-6m8e_.js.map} +1 -1
  195. package/dashboard/dist/assets/index-CkeSxuZ0.js +2 -0
  196. package/dashboard/dist/assets/{index-B5JgykZD.js.map → index-CkeSxuZ0.js.map} +1 -1
  197. package/dashboard/dist/assets/{index-BW7-Q9Qf.js → index-CxVGlkK6.js} +2 -2
  198. package/dashboard/dist/assets/{index-BW7-Q9Qf.js.map → index-CxVGlkK6.js.map} +1 -1
  199. package/dashboard/dist/assets/index-D2CfSx_x.js +5 -0
  200. package/dashboard/dist/assets/index-D2CfSx_x.js.map +1 -0
  201. package/dashboard/dist/assets/{index-4zRmxPwA.js → index-DFJF8aWz.js} +3 -3
  202. package/dashboard/dist/assets/{index-4zRmxPwA.js.map → index-DFJF8aWz.js.map} +1 -1
  203. package/dashboard/dist/assets/index-DYKRtstT.js +2 -0
  204. package/dashboard/dist/assets/index-DYKRtstT.js.map +1 -0
  205. package/dashboard/dist/assets/index-DY_hlZCQ.js +2 -0
  206. package/dashboard/dist/assets/index-DY_hlZCQ.js.map +1 -0
  207. package/dashboard/dist/assets/{index-KkikDsJ2.js → index-DbZpL10z.js} +2 -2
  208. package/dashboard/dist/assets/{index-KkikDsJ2.js.map → index-DbZpL10z.js.map} +1 -1
  209. package/dashboard/dist/assets/index-Dh3CVChT.js +63 -0
  210. package/dashboard/dist/assets/index-Dh3CVChT.js.map +1 -0
  211. package/dashboard/dist/assets/{index-peRHL38z.js → index-DqsLfO-0.js} +2 -2
  212. package/dashboard/dist/assets/{index-peRHL38z.js.map → index-DqsLfO-0.js.map} +1 -1
  213. package/dashboard/dist/assets/{index-C-T1Wq0Z.js → index-iHV3d7o3.js} +2 -2
  214. package/dashboard/dist/assets/{index-C-T1Wq0Z.js.map → index-iHV3d7o3.js.map} +1 -1
  215. package/dashboard/dist/assets/{index-CeM012DJ.js → index-ymm58cTW.js} +2 -2
  216. package/dashboard/dist/assets/{index-CeM012DJ.js.map → index-ymm58cTW.js.map} +1 -1
  217. package/dashboard/dist/assets/{knowledge-DO8idX0e.js → knowledge-Bawo8u7l.js} +2 -2
  218. package/dashboard/dist/assets/{knowledge-DO8idX0e.js.map → knowledge-Bawo8u7l.js.map} +1 -1
  219. package/dashboard/dist/assets/{mcp-BGpQlkFp.js → mcp-CKIpbpRZ.js} +2 -2
  220. package/dashboard/dist/assets/{mcp-BGpQlkFp.js.map → mcp-CKIpbpRZ.js.map} +1 -1
  221. package/dashboard/dist/assets/{mcp-query-DT0SCB9l.js → mcp-query-vld4hYxQ.js} +2 -2
  222. package/dashboard/dist/assets/{mcp-query-DT0SCB9l.js.map → mcp-query-vld4hYxQ.js.map} +1 -1
  223. package/dashboard/dist/assets/personas-BsARc3df.js +2 -0
  224. package/dashboard/dist/assets/personas-BsARc3df.js.map +1 -0
  225. package/dashboard/dist/assets/{pipelines-CYk7cN6-.js → pipelines-BTSL-5ko.js} +2 -2
  226. package/dashboard/dist/assets/{pipelines-CYk7cN6-.js.map → pipelines-BTSL-5ko.js.map} +1 -1
  227. package/dashboard/dist/assets/{tasks-B4Y5xbF0.js → tasks-DggQdNbj.js} +2 -2
  228. package/dashboard/dist/assets/{tasks-B4Y5xbF0.js.map → tasks-DggQdNbj.js.map} +1 -1
  229. package/dashboard/dist/assets/{topics-CVnqDw3B.js → topics-DzAKa34X.js} +2 -2
  230. package/dashboard/dist/assets/{topics-CVnqDw3B.js.map → topics-DzAKa34X.js.map} +1 -1
  231. package/dashboard/dist/assets/useEventHooks-5uI9oI5W.js +2 -0
  232. package/dashboard/dist/assets/useEventHooks-5uI9oI5W.js.map +1 -0
  233. package/dashboard/dist/assets/{useNamespace-B1t3hsGf.js → useNamespace-CEKzFIqe.js} +2 -2
  234. package/dashboard/dist/assets/{useNamespace-B1t3hsGf.js.map → useNamespace-CEKzFIqe.js.map} +1 -1
  235. package/dashboard/dist/assets/{useYamlActivityEvents-B8Ru07xQ.js → useYamlActivityEvents-hHgX1Z50.js} +2 -2
  236. package/dashboard/dist/assets/{useYamlActivityEvents-B8Ru07xQ.js.map → useYamlActivityEvents-hHgX1Z50.js.map} +1 -1
  237. package/dashboard/dist/assets/{users-C8H4jjTq.js → users-AO8lUyNr.js} +2 -2
  238. package/dashboard/dist/assets/{users-C8H4jjTq.js.map → users-AO8lUyNr.js.map} +1 -1
  239. package/dashboard/dist/assets/{vendor-icons-BE_WrH2z.js → vendor-icons-DN9yGSuM.js} +134 -129
  240. package/dashboard/dist/assets/vendor-icons-DN9yGSuM.js.map +1 -0
  241. package/dashboard/dist/assets/{workflows-BAdI5k--.js → workflows-PtnlRK09.js} +2 -2
  242. package/dashboard/dist/assets/{workflows-BAdI5k--.js.map → workflows-PtnlRK09.js.map} +1 -1
  243. package/dashboard/dist/assets/{yaml-workflows-Bf2aQS-M.js → yaml-workflows-DPu3Y8vG.js} +2 -2
  244. package/dashboard/dist/assets/{yaml-workflows-Bf2aQS-M.js.map → yaml-workflows-DPu3Y8vG.js.map} +1 -1
  245. package/dashboard/dist/index.html +3 -3
  246. package/docs/api/http/escalations.md +1 -1
  247. package/docs/api/http/personas.md +160 -0
  248. package/docs/api/sdk/escalations.md +2 -2
  249. package/docs/cli.md +12 -0
  250. package/docs/dashboard.md +16 -1
  251. package/docs/design-principles.md +9 -5
  252. package/docs/events.md +4 -2
  253. package/docs/hitl/escalation.md +1 -1
  254. package/docs/hitl/x-lt-list-schema.md +12 -5
  255. package/docs/iam.md +47 -0
  256. package/docs/sdk.md +1 -0
  257. package/package.json +1 -1
  258. package/dashboard/dist/assets/AgentDetailPage-C5AnjpD9.js +0 -4
  259. package/dashboard/dist/assets/AgentDetailPage-C5AnjpD9.js.map +0 -1
  260. package/dashboard/dist/assets/AvailableEscalationsPage-Bk7fFV2e.js.map +0 -1
  261. package/dashboard/dist/assets/CountdownTimer-D-CpxYDO.js +0 -2
  262. package/dashboard/dist/assets/CountdownTimer-D-CpxYDO.js.map +0 -1
  263. package/dashboard/dist/assets/CustomDurationPicker-_KceggR9.js +0 -2
  264. package/dashboard/dist/assets/CustomDurationPicker-_KceggR9.js.map +0 -1
  265. package/dashboard/dist/assets/ElapsedCell-Cflz7Pb5.js +0 -2
  266. package/dashboard/dist/assets/HomePage-NMLaXTvf.js +0 -2
  267. package/dashboard/dist/assets/HomePage-NMLaXTvf.js.map +0 -1
  268. package/dashboard/dist/assets/McpQueryDetailPage-Bqh8GChr.js +0 -5
  269. package/dashboard/dist/assets/McpQueryDetailPage-Bqh8GChr.js.map +0 -1
  270. package/dashboard/dist/assets/McpQueryPage-CqFkBwBR.js +0 -2
  271. package/dashboard/dist/assets/McpQueryPage-CqFkBwBR.js.map +0 -1
  272. package/dashboard/dist/assets/OperatorDashboard-DwbPAPaf.js +0 -2
  273. package/dashboard/dist/assets/OperatorDashboard-DwbPAPaf.js.map +0 -1
  274. package/dashboard/dist/assets/ProcessDetailPage-ChGWcwSp.js +0 -2
  275. package/dashboard/dist/assets/ProcessDetailPage-ChGWcwSp.js.map +0 -1
  276. package/dashboard/dist/assets/RoleDetailPage-GJ_coHsM.js +0 -8
  277. package/dashboard/dist/assets/RoleDetailPage-GJ_coHsM.js.map +0 -1
  278. package/dashboard/dist/assets/RolesPage-CPKw336d.js +0 -2
  279. package/dashboard/dist/assets/RolesPage-CPKw336d.js.map +0 -1
  280. package/dashboard/dist/assets/StatCard-DBANWIss.js +0 -2
  281. package/dashboard/dist/assets/StatCard-DBANWIss.js.map +0 -1
  282. package/dashboard/dist/assets/StreamMessageDetail-BObbEz7P.js.map +0 -1
  283. package/dashboard/dist/assets/TopicsPage-C5THflVQ.js +0 -2
  284. package/dashboard/dist/assets/index-B5JgykZD.js +0 -2
  285. package/dashboard/dist/assets/index-CKxFjMtw.js +0 -63
  286. package/dashboard/dist/assets/index-CKxFjMtw.js.map +0 -1
  287. package/dashboard/dist/assets/index-Pmvw74oY.css +0 -1
  288. package/dashboard/dist/assets/index-_Q1Wf2A8.js +0 -2
  289. package/dashboard/dist/assets/index-_Q1Wf2A8.js.map +0 -1
  290. package/dashboard/dist/assets/index-jkVeGCkD.js +0 -5
  291. package/dashboard/dist/assets/index-jkVeGCkD.js.map +0 -1
  292. package/dashboard/dist/assets/index-oHU-ramT.js +0 -2
  293. package/dashboard/dist/assets/index-oHU-ramT.js.map +0 -1
  294. package/dashboard/dist/assets/useEventHooks-D8bVEx-w.js +0 -2
  295. package/dashboard/dist/assets/useEventHooks-D8bVEx-w.js.map +0 -1
  296. package/dashboard/dist/assets/vendor-icons-BE_WrH2z.js.map +0 -1
@@ -74,6 +74,7 @@ async function createEscalation(input) {
74
74
  workflowName: escalation.workflow_type || '',
75
75
  taskQueue: escalation.task_queue || '',
76
76
  escalationId: escalation.id,
77
+ role: escalation.role,
77
78
  status: 'pending',
78
79
  data: { type: input.type, role: input.role },
79
80
  });
@@ -97,6 +98,7 @@ async function claimEscalation(id, userId, durationMinutes = 30) {
97
98
  workflowName: escalation.workflow_type || '',
98
99
  taskQueue: escalation.task_queue || '',
99
100
  escalationId: escalation.id,
101
+ role: escalation.role,
100
102
  status: 'claimed',
101
103
  data: { assigned_to: userId },
102
104
  });
@@ -137,6 +139,7 @@ async function resolveEscalation(id, resolverPayload, metadata, assertClaim, res
137
139
  workflowName: escalation.workflow_type || '',
138
140
  taskQueue: escalation.task_queue || '',
139
141
  escalationId: escalation.id,
142
+ role: escalation.role,
140
143
  status: 'resolved',
141
144
  data: {},
142
145
  });
@@ -170,6 +173,7 @@ async function resolveEscalationsByIds(ids, resolverPayload, metadata) {
170
173
  workflowName: escalation.workflow_type || '',
171
174
  taskQueue: escalation.task_queue || '',
172
175
  escalationId: escalation.id,
176
+ role: escalation.role,
173
177
  status: 'resolved',
174
178
  data: {},
175
179
  });
@@ -209,6 +213,7 @@ async function resolveEscalationsAllOrNone(items, metadata, assertAssignee, reso
209
213
  workflowName: escalation.workflow_type || '',
210
214
  taskQueue: escalation.task_queue || '',
211
215
  escalationId: escalation.id,
216
+ role: escalation.role,
212
217
  status: 'resolved',
213
218
  data: {},
214
219
  });
@@ -276,6 +281,7 @@ async function cancelEscalation(id) {
276
281
  workflowName: cancelled.workflow_type || '',
277
282
  taskQueue: cancelled.task_queue || '',
278
283
  escalationId: cancelled.id,
284
+ role: cancelled.role,
279
285
  status: 'cancelled',
280
286
  data: { reason: 'workflow_terminated' },
281
287
  });
@@ -335,6 +341,7 @@ async function releaseEscalation(id, userId) {
335
341
  workflowName: released.workflow_type || '',
336
342
  taskQueue: released.task_queue || '',
337
343
  escalationId: released.id,
344
+ role: released.role,
338
345
  status: 'released',
339
346
  data: { released_by: userId },
340
347
  });
@@ -358,8 +365,36 @@ async function releaseExpiredClaims() {
358
365
  */
359
366
  async function escalateToRole(id, targetRole) {
360
367
  const client = await (0, client_1.escalations)();
368
+ // The departing role is read before the move: the returned entry already
369
+ // carries the target role, and role-scoped subscribers of the OLD queue
370
+ // must see the item leave just as the new queue sees it arrive.
371
+ const before = await client.get(id);
372
+ const fromRole = before?.role ?? null;
361
373
  const entry = await client.escalateToRole({ id, targetRole });
362
- return entry ? (0, map_1.toEscalationRecord)(entry) : null;
374
+ if (!entry)
375
+ return null;
376
+ const escalation = (0, map_1.toEscalationRecord)(entry);
377
+ // One reassignment, two subjects: the same event publishes under the
378
+ // departing role and the arriving role, so a subscriber scoped to either
379
+ // queue observes the move. `data` names both ends for consumers that
380
+ // only see one side.
381
+ const rolesToNotify = fromRole && fromRole !== escalation.role
382
+ ? [fromRole, escalation.role]
383
+ : [escalation.role];
384
+ for (const role of rolesToNotify) {
385
+ publishEscalationChange({
386
+ type: 'escalation.reassigned',
387
+ source: 'service',
388
+ workflowId: escalation.workflow_id || '',
389
+ workflowName: escalation.workflow_type || '',
390
+ taskQueue: escalation.task_queue || '',
391
+ escalationId: escalation.id,
392
+ role,
393
+ status: 'pending',
394
+ data: { from_role: fromRole, to_role: escalation.role },
395
+ });
396
+ }
397
+ return escalation;
363
398
  }
364
399
  async function getEscalation(id) {
365
400
  const client = await (0, client_1.escalations)();
@@ -402,7 +437,7 @@ async function cancelEscalationsByWorkflowId(workflowId) {
402
437
  SET status = 'cancelled', updated_at = NOW()
403
438
  WHERE workflow_id = $1
404
439
  AND status = 'pending'
405
- RETURNING id, workflow_id, workflow_type, task_queue`, [workflowId]);
440
+ RETURNING id, workflow_id, workflow_type, task_queue, role`, [workflowId]);
406
441
  for (const row of rows) {
407
442
  publishEscalationChange({
408
443
  type: 'escalation.cancelled',
@@ -411,6 +446,7 @@ async function cancelEscalationsByWorkflowId(workflowId) {
411
446
  workflowName: row.workflow_type || '',
412
447
  taskQueue: row.task_queue || '',
413
448
  escalationId: row.id,
449
+ role: row.role || '',
414
450
  status: 'cancelled',
415
451
  data: { reason: 'workflow_terminated' },
416
452
  });
@@ -498,6 +534,7 @@ async function claimByMetadata(key, value, userId, durationMinutes = 30, metadat
498
534
  workflowName: escalation.workflow_type || '',
499
535
  taskQueue: escalation.task_queue || '',
500
536
  escalationId: escalation.id,
537
+ role: escalation.role,
501
538
  status: 'claimed',
502
539
  data: { assigned_to: userId },
503
540
  });
@@ -556,6 +593,7 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
556
593
  workflowName: escalation.workflow_type || '',
557
594
  taskQueue: escalation.task_queue || '',
558
595
  escalationId: escalation.id,
596
+ role: escalation.role,
559
597
  status: 'resolved',
560
598
  data: { resolved_by: userId },
561
599
  });
@@ -21,7 +21,7 @@ export type ConditionEscalationConfig = Types.ConditionQueueConfig & {
21
21
  * written inside this workflow's Leg1 checkpoint (one commit, crash-safe — no
22
22
  * separate create activity, no enrich). `signal_key` is the signal id, so the
23
23
  * dashboard resolve endpoint (Path 0), `resolveEscalationBySignalKey`, and any
24
- * webhook resume the SAME job in place. `system.escalation.{id}.created` fires
24
+ * webhook resume the SAME job in place. `system.escalation.{role}.{id}.created` fires
25
25
  * from the engine automatically.
26
26
  *
27
27
  * ```typescript
@@ -60,7 +60,7 @@ function toEngineConfig(escalation) {
60
60
  * written inside this workflow's Leg1 checkpoint (one commit, crash-safe — no
61
61
  * separate create activity, no enrich). `signal_key` is the signal id, so the
62
62
  * dashboard resolve endpoint (Path 0), `resolveEscalationBySignalKey`, and any
63
- * webhook resume the SAME job in place. `system.escalation.{id}.created` fires
63
+ * webhook resume the SAME job in place. `system.escalation.{role}.{id}.created` fires
64
64
  * from the engine automatically.
65
65
  *
66
66
  * ```typescript
@@ -0,0 +1,92 @@
1
+ import type { LTPersonaRecord, LTPersonaRelationship, LTPersonaSpec, LTUserPersona, LTComposedRoleScope } from '../../types';
2
+ import type { CreatePersonaInput, UpdatePersonaInput } from './types';
3
+ export { PERSONA_RELATIONSHIPS, normalizeRelationship, relationshipToWriteScope, } from './types';
4
+ export type { CreatePersonaInput, UpdatePersonaInput } from './types';
5
+ /** Counts from one membership reconciliation pass. */
6
+ export interface PersonaRecomputeResult {
7
+ granted: number;
8
+ refreshed: number;
9
+ raised: number;
10
+ removed: number;
11
+ }
12
+ /** An assignee row on the persona detail surface. */
13
+ export interface PersonaAssignee {
14
+ id: string;
15
+ external_id: string;
16
+ display_name: string | null;
17
+ email: string | null;
18
+ assigned_at: Date;
19
+ }
20
+ export declare function listPersonas(): Promise<LTPersonaRecord[]>;
21
+ export declare function getPersona(key: string): Promise<(LTPersonaRecord & {
22
+ assignees: PersonaAssignee[];
23
+ }) | null>;
24
+ export declare function createPersona(input: CreatePersonaInput): Promise<LTPersonaRecord>;
25
+ export declare function updatePersona(key: string, input: UpdatePersonaInput): Promise<LTPersonaRecord | null>;
26
+ /**
27
+ * Delete a persona. Holds are removed and memberships reconciled BEFORE the
28
+ * persona row is deleted, so rows it sustained are dropped (or re-homed to a
29
+ * sibling persona) rather than silently converting into direct grants.
30
+ */
31
+ export declare function deletePersona(key: string): Promise<{
32
+ deleted: boolean;
33
+ recompute?: PersonaRecomputeResult;
34
+ }>;
35
+ /**
36
+ * Link a role to a persona (or update the link's relationship). The role is
37
+ * ensured as an FK target, and every current holder's memberships are
38
+ * reconciled in the same transaction.
39
+ */
40
+ export declare function linkPersonaRole(key: string, role: string, relationship: LTPersonaRelationship): Promise<{
41
+ role: string;
42
+ relationship: LTPersonaRelationship;
43
+ recompute: PersonaRecomputeResult;
44
+ } | null>;
45
+ /** Unlink a role from a persona and reconcile every holder's memberships. */
46
+ export declare function unlinkPersonaRole(key: string, role: string): Promise<{
47
+ unlinked: boolean;
48
+ personaFound: boolean;
49
+ recompute?: PersonaRecomputeResult;
50
+ }>;
51
+ /**
52
+ * Assign a persona to a user. Idempotent: re-assigning overlays fresh — the
53
+ * user's memberships are reconciled against the persona's CURRENT role links
54
+ * (union with every other persona they hold; direct grants only ever raised,
55
+ * never lowered or removed).
56
+ */
57
+ export declare function assignPersona(userId: string, key: string): Promise<PersonaRecomputeResult | null>;
58
+ /**
59
+ * Unassign a persona from a user. Removes only memberships the persona
60
+ * sustains — rows another held persona still grants are re-homed to it, and
61
+ * direct grants are never touched.
62
+ */
63
+ export declare function unassignPersona(userId: string, key: string): Promise<{
64
+ unassigned: boolean;
65
+ personaFound: boolean;
66
+ recompute?: PersonaRecomputeResult;
67
+ }>;
68
+ /** The personas a user holds plus the composed role/scope map their memberships form. */
69
+ export declare function getUserPersonas(userId: string): Promise<{
70
+ personas: LTUserPersona[];
71
+ roles: LTComposedRoleScope[];
72
+ }>;
73
+ /**
74
+ * Declarative, idempotent persona seeding — the static-config twin of the API
75
+ * surface (like roles + default_pins in the same seed pass). Each spec is
76
+ * authoritative for its persona: title/description overlaid, role links synced
77
+ * (links absent from the spec are pruned), linked roles ensured as FK targets,
78
+ * and every holder of a seeded persona reconciled — re-running the seed after
79
+ * editing a spec re-applies it to everyone holding the persona.
80
+ *
81
+ * Relationships must be canonical (normalize aliases at the API boundary).
82
+ */
83
+ export declare function seedPersonas(specs: (Omit<LTPersonaSpec, 'roles'> & {
84
+ roles: {
85
+ role: string;
86
+ relationship: LTPersonaRelationship;
87
+ }[];
88
+ })[]): Promise<{
89
+ personas: number;
90
+ links: number;
91
+ recompute: PersonaRecomputeResult;
92
+ }>;
@@ -0,0 +1,217 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.relationshipToWriteScope = exports.normalizeRelationship = exports.PERSONA_RELATIONSHIPS = void 0;
4
+ exports.listPersonas = listPersonas;
5
+ exports.getPersona = getPersona;
6
+ exports.createPersona = createPersona;
7
+ exports.updatePersona = updatePersona;
8
+ exports.deletePersona = deletePersona;
9
+ exports.linkPersonaRole = linkPersonaRole;
10
+ exports.unlinkPersonaRole = unlinkPersonaRole;
11
+ exports.assignPersona = assignPersona;
12
+ exports.unassignPersona = unassignPersona;
13
+ exports.getUserPersonas = getUserPersonas;
14
+ exports.seedPersonas = seedPersonas;
15
+ const db_1 = require("../../lib/db");
16
+ const sql_1 = require("./sql");
17
+ var types_1 = require("./types");
18
+ Object.defineProperty(exports, "PERSONA_RELATIONSHIPS", { enumerable: true, get: function () { return types_1.PERSONA_RELATIONSHIPS; } });
19
+ Object.defineProperty(exports, "normalizeRelationship", { enumerable: true, get: function () { return types_1.normalizeRelationship; } });
20
+ Object.defineProperty(exports, "relationshipToWriteScope", { enumerable: true, get: function () { return types_1.relationshipToWriteScope; } });
21
+ const EMPTY_RECOMPUTE = { granted: 0, refreshed: 0, raised: 0, removed: 0 };
22
+ // ─── Internal helpers ─────────────────────────────────────────────────────────
23
+ /** Run `work` inside a single transaction on a dedicated client. */
24
+ async function withTransaction(work) {
25
+ const client = await (0, db_1.getPool)().connect();
26
+ try {
27
+ await client.query('BEGIN');
28
+ const result = await work((text, params) => client.query(text, params));
29
+ await client.query('COMMIT');
30
+ return result;
31
+ }
32
+ catch (err) {
33
+ await client.query('ROLLBACK').catch(() => { });
34
+ throw err;
35
+ }
36
+ finally {
37
+ client.release();
38
+ }
39
+ }
40
+ /** Reconcile lt_user_roles for a set of users against their held personas. */
41
+ async function recomputeMemberships(query, userIds) {
42
+ if (userIds.length === 0)
43
+ return { ...EMPTY_RECOMPUTE };
44
+ const { rows } = await query(sql_1.RECOMPUTE_PERSONA_MEMBERSHIPS, [userIds]);
45
+ return rows[0];
46
+ }
47
+ async function resolvePersonaId(query, key) {
48
+ const { rows } = await query(sql_1.GET_PERSONA_ID_BY_KEY, [key]);
49
+ return rows[0]?.id ?? null;
50
+ }
51
+ // ─── Persona CRUD ─────────────────────────────────────────────────────────────
52
+ async function listPersonas() {
53
+ const { rows } = await (0, db_1.getPool)().query(sql_1.LIST_PERSONAS);
54
+ return rows;
55
+ }
56
+ async function getPersona(key) {
57
+ const pool = (0, db_1.getPool)();
58
+ const { rows } = await pool.query(sql_1.GET_PERSONA_BY_KEY, [key]);
59
+ if (rows.length === 0)
60
+ return null;
61
+ const { rows: assignees } = await pool.query(sql_1.GET_PERSONA_ASSIGNEES, [rows[0].id]);
62
+ return { ...rows[0], assignees };
63
+ }
64
+ async function createPersona(input) {
65
+ const { rows } = await (0, db_1.getPool)().query(sql_1.CREATE_PERSONA, [
66
+ input.key,
67
+ input.title ?? null,
68
+ input.description ?? null,
69
+ ]);
70
+ return { ...rows[0], roles: [], user_count: 0 };
71
+ }
72
+ async function updatePersona(key, input) {
73
+ const { rows } = await (0, db_1.getPool)().query(sql_1.UPDATE_PERSONA, [
74
+ key,
75
+ input.title !== undefined, input.title ?? null,
76
+ input.description !== undefined, input.description ?? null,
77
+ ]);
78
+ if (rows.length === 0)
79
+ return null;
80
+ const detail = await getPersona(key);
81
+ return detail;
82
+ }
83
+ /**
84
+ * Delete a persona. Holds are removed and memberships reconciled BEFORE the
85
+ * persona row is deleted, so rows it sustained are dropped (or re-homed to a
86
+ * sibling persona) rather than silently converting into direct grants.
87
+ */
88
+ async function deletePersona(key) {
89
+ return withTransaction(async (query) => {
90
+ const personaId = await resolvePersonaId(query, key);
91
+ if (!personaId)
92
+ return { deleted: false };
93
+ const { rows: holders } = await query(`DELETE FROM lt_user_personas WHERE persona_id = $1 RETURNING user_id`, [personaId]);
94
+ // Guard against drift: also reconcile any user with a row still sustained
95
+ // by this persona, even if no hold record remained.
96
+ const { rows: sustained } = await query(sql_1.LIST_SUSTAINED_USER_IDS, [personaId]);
97
+ const affected = [...new Set([...holders, ...sustained].map((r) => r.user_id))];
98
+ const recompute = await recomputeMemberships(query, affected);
99
+ await query(sql_1.DELETE_PERSONA, [personaId]);
100
+ return { deleted: true, recompute };
101
+ });
102
+ }
103
+ // ─── Role links ───────────────────────────────────────────────────────────────
104
+ /**
105
+ * Link a role to a persona (or update the link's relationship). The role is
106
+ * ensured as an FK target, and every current holder's memberships are
107
+ * reconciled in the same transaction.
108
+ */
109
+ async function linkPersonaRole(key, role, relationship) {
110
+ return withTransaction(async (query) => {
111
+ const personaId = await resolvePersonaId(query, key);
112
+ if (!personaId)
113
+ return null;
114
+ await query(sql_1.ENSURE_ROLES_EXIST, [[role]]);
115
+ const { rows } = await query(sql_1.UPSERT_PERSONA_ROLE, [personaId, role, relationship]);
116
+ const { rows: holders } = await query(sql_1.LIST_PERSONA_HOLDER_IDS, [personaId]);
117
+ const recompute = await recomputeMemberships(query, holders.map((r) => r.user_id));
118
+ return { ...rows[0], recompute };
119
+ });
120
+ }
121
+ /** Unlink a role from a persona and reconcile every holder's memberships. */
122
+ async function unlinkPersonaRole(key, role) {
123
+ return withTransaction(async (query) => {
124
+ const personaId = await resolvePersonaId(query, key);
125
+ if (!personaId)
126
+ return { unlinked: false, personaFound: false };
127
+ const { rowCount } = await query(sql_1.DELETE_PERSONA_ROLE, [personaId, role]);
128
+ if ((rowCount ?? 0) === 0)
129
+ return { unlinked: false, personaFound: true };
130
+ const { rows: holders } = await query(sql_1.LIST_PERSONA_HOLDER_IDS, [personaId]);
131
+ const recompute = await recomputeMemberships(query, holders.map((r) => r.user_id));
132
+ return { unlinked: true, personaFound: true, recompute };
133
+ });
134
+ }
135
+ // ─── Assignment ───────────────────────────────────────────────────────────────
136
+ /**
137
+ * Assign a persona to a user. Idempotent: re-assigning overlays fresh — the
138
+ * user's memberships are reconciled against the persona's CURRENT role links
139
+ * (union with every other persona they hold; direct grants only ever raised,
140
+ * never lowered or removed).
141
+ */
142
+ async function assignPersona(userId, key) {
143
+ return withTransaction(async (query) => {
144
+ const personaId = await resolvePersonaId(query, key);
145
+ if (!personaId)
146
+ return null;
147
+ await query(sql_1.INSERT_USER_PERSONA, [userId, personaId]);
148
+ return recomputeMemberships(query, [userId]);
149
+ });
150
+ }
151
+ /**
152
+ * Unassign a persona from a user. Removes only memberships the persona
153
+ * sustains — rows another held persona still grants are re-homed to it, and
154
+ * direct grants are never touched.
155
+ */
156
+ async function unassignPersona(userId, key) {
157
+ return withTransaction(async (query) => {
158
+ const personaId = await resolvePersonaId(query, key);
159
+ if (!personaId)
160
+ return { unassigned: false, personaFound: false };
161
+ const { rowCount } = await query(sql_1.DELETE_USER_PERSONA, [userId, personaId]);
162
+ if ((rowCount ?? 0) === 0)
163
+ return { unassigned: false, personaFound: true };
164
+ const recompute = await recomputeMemberships(query, [userId]);
165
+ return { unassigned: true, personaFound: true, recompute };
166
+ });
167
+ }
168
+ /** The personas a user holds plus the composed role/scope map their memberships form. */
169
+ async function getUserPersonas(userId) {
170
+ const pool = (0, db_1.getPool)();
171
+ const [{ rows: personas }, { rows: roles }] = await Promise.all([
172
+ pool.query(sql_1.GET_USER_PERSONAS, [userId]),
173
+ pool.query(sql_1.GET_USER_COMPOSED_ROLES, [userId]),
174
+ ]);
175
+ return { personas, roles };
176
+ }
177
+ // ─── Declarative seed ─────────────────────────────────────────────────────────
178
+ /**
179
+ * Declarative, idempotent persona seeding — the static-config twin of the API
180
+ * surface (like roles + default_pins in the same seed pass). Each spec is
181
+ * authoritative for its persona: title/description overlaid, role links synced
182
+ * (links absent from the spec are pruned), linked roles ensured as FK targets,
183
+ * and every holder of a seeded persona reconciled — re-running the seed after
184
+ * editing a spec re-applies it to everyone holding the persona.
185
+ *
186
+ * Relationships must be canonical (normalize aliases at the API boundary).
187
+ */
188
+ async function seedPersonas(specs) {
189
+ if (specs.length === 0)
190
+ return { personas: 0, links: 0, recompute: { ...EMPTY_RECOMPUTE } };
191
+ return withTransaction(async (query) => {
192
+ const allRoles = [...new Set(specs.flatMap((s) => s.roles.map((r) => r.role)))];
193
+ if (allRoles.length > 0) {
194
+ await query(sql_1.ENSURE_ROLES_EXIST, [allRoles]);
195
+ }
196
+ const personaIds = [];
197
+ let links = 0;
198
+ for (const spec of specs) {
199
+ const { rows } = await query(sql_1.UPSERT_PERSONA, [
200
+ spec.key,
201
+ spec.title ?? null,
202
+ spec.description ?? null,
203
+ ]);
204
+ const personaId = rows[0].id;
205
+ personaIds.push(personaId);
206
+ await query(sql_1.SYNC_PERSONA_ROLES, [
207
+ personaId,
208
+ spec.roles.map((r) => r.role),
209
+ spec.roles.map((r) => r.relationship),
210
+ ]);
211
+ links += spec.roles.length;
212
+ }
213
+ const { rows: holders } = await query('SELECT DISTINCT user_id FROM lt_user_personas WHERE persona_id = ANY($1::uuid[])', [personaIds]);
214
+ const recompute = await recomputeMemberships(query, holders.map((r) => r.user_id));
215
+ return { personas: specs.length, links, recompute };
216
+ });
217
+ }
@@ -0,0 +1,24 @@
1
+ export declare const LIST_PERSONAS = "SELECT p.id, p.key, p.title, p.description, r.roles, u.user_count, p.created_at, p.updated_at\n FROM lt_personas p\n LEFT JOIN LATERAL (\n SELECT COALESCE(json_agg(json_build_object('role', pr.role, 'relationship', pr.relationship)\n ORDER BY pr.created_at, pr.role), '[]'::json) AS roles\n FROM lt_persona_roles pr WHERE pr.persona_id = p.id\n ) r ON true\n LEFT JOIN LATERAL (\n SELECT COUNT(*)::int AS user_count FROM lt_user_personas up WHERE up.persona_id = p.id\n ) u ON true\n ORDER BY p.key";
2
+ export declare const GET_PERSONA_BY_KEY = "SELECT p.id, p.key, p.title, p.description, r.roles, u.user_count, p.created_at, p.updated_at\n FROM lt_personas p\n LEFT JOIN LATERAL (\n SELECT COALESCE(json_agg(json_build_object('role', pr.role, 'relationship', pr.relationship)\n ORDER BY pr.created_at, pr.role), '[]'::json) AS roles\n FROM lt_persona_roles pr WHERE pr.persona_id = p.id\n ) r ON true\n LEFT JOIN LATERAL (\n SELECT COUNT(*)::int AS user_count FROM lt_user_personas up WHERE up.persona_id = p.id\n ) u ON true\n WHERE p.key = $1";
3
+ export declare const CREATE_PERSONA = "INSERT INTO lt_personas (key, title, description) VALUES ($1, $2, $3)\n RETURNING id, key, title, description, created_at, updated_at";
4
+ /** Declarative upsert for the seed pass — the spec is authoritative for title/description. */
5
+ export declare const UPSERT_PERSONA = "INSERT INTO lt_personas (key, title, description) VALUES ($1, $2, $3)\n ON CONFLICT (key) DO UPDATE SET\n title = EXCLUDED.title,\n description = EXCLUDED.description,\n updated_at = NOW()\n RETURNING id";
6
+ /** PATCH semantics: $2/$4 are set-sentinels gating whether $3/$5 write. */
7
+ export declare const UPDATE_PERSONA = "UPDATE lt_personas SET\n title = CASE WHEN $2 THEN $3 ELSE title END,\n description = CASE WHEN $4 THEN $5 ELSE description END,\n updated_at = NOW()\n WHERE key = $1\n RETURNING id, key, title, description, created_at, updated_at";
8
+ export declare const DELETE_PERSONA = "DELETE FROM lt_personas WHERE id = $1";
9
+ export declare const GET_PERSONA_ASSIGNEES = "SELECT us.id, us.external_id, us.display_name, us.email, up.created_at AS assigned_at\n FROM lt_user_personas up\n JOIN lt_users us ON us.id = up.user_id\n WHERE up.persona_id = $1\n ORDER BY up.created_at, us.external_id";
10
+ export declare const UPSERT_PERSONA_ROLE = "INSERT INTO lt_persona_roles (persona_id, role, relationship) VALUES ($1, $2, $3)\n ON CONFLICT (persona_id, role) DO UPDATE SET relationship = EXCLUDED.relationship\n RETURNING role, relationship";
11
+ export declare const DELETE_PERSONA_ROLE = "DELETE FROM lt_persona_roles WHERE persona_id = $1 AND role = $2";
12
+ /** Replace a persona's links declaratively: prune links not in $2, upsert the rest. */
13
+ export declare const SYNC_PERSONA_ROLES = "WITH incoming AS (\n SELECT x.role, x.relationship\n FROM unnest($2::text[], $3::text[]) AS x(role, relationship)\n ), pruned AS (\n DELETE FROM lt_persona_roles pr\n WHERE pr.persona_id = $1 AND pr.role NOT IN (SELECT role FROM incoming)\n )\n INSERT INTO lt_persona_roles (persona_id, role, relationship)\n SELECT $1, role, relationship FROM incoming\n ON CONFLICT (persona_id, role) DO UPDATE SET relationship = EXCLUDED.relationship";
14
+ export declare const INSERT_USER_PERSONA = "INSERT INTO lt_user_personas (user_id, persona_id) VALUES ($1, $2)\n ON CONFLICT DO NOTHING";
15
+ export declare const DELETE_USER_PERSONA = "DELETE FROM lt_user_personas WHERE user_id = $1 AND persona_id = $2";
16
+ export declare const LIST_PERSONA_HOLDER_IDS = "SELECT user_id FROM lt_user_personas WHERE persona_id = $1";
17
+ /** Users with a membership row still sustained by this persona (drift guard for delete). */
18
+ export declare const LIST_SUSTAINED_USER_IDS = "SELECT DISTINCT user_id FROM lt_user_roles WHERE granted_by_persona = $1";
19
+ export declare const RECOMPUTE_PERSONA_MEMBERSHIPS = "WITH affected AS (\n SELECT unnest($1::uuid[]) AS user_id\n ), grants AS (\n SELECT up.user_id, pr.role,\n MAX(CASE pr.relationship WHEN 'write-all' THEN 2 WHEN 'write-self' THEN 1 ELSE 0 END) AS write_rank,\n (MIN(pr.persona_id::text))::uuid AS sustaining_persona\n FROM lt_user_personas up\n JOIN lt_persona_roles pr ON pr.persona_id = up.persona_id\n JOIN affected a ON a.user_id = up.user_id\n GROUP BY up.user_id, pr.role\n ), removed AS (\n DELETE FROM lt_user_roles ur\n USING affected a\n WHERE ur.user_id = a.user_id\n AND ur.granted_by_persona IS NOT NULL\n AND NOT EXISTS (SELECT 1 FROM grants g WHERE g.user_id = ur.user_id AND g.role = ur.role)\n RETURNING ur.user_id, ur.role\n ), refreshed AS (\n UPDATE lt_user_roles ur\n SET read_scope = 'all',\n write_scope = CASE g.write_rank WHEN 2 THEN 'all' WHEN 1 THEN 'self' ELSE 'none' END,\n granted_by_persona = g.sustaining_persona\n FROM grants g\n WHERE ur.user_id = g.user_id AND ur.role = g.role\n AND ur.granted_by_persona IS NOT NULL\n RETURNING ur.user_id, ur.role\n ), raised AS (\n UPDATE lt_user_roles ur\n SET read_scope = 'all',\n write_scope = CASE\n WHEN g.write_rank = 2 THEN 'all'\n WHEN g.write_rank = 1 AND ur.write_scope = 'none' THEN 'self'\n ELSE ur.write_scope\n END\n FROM grants g\n WHERE ur.user_id = g.user_id AND ur.role = g.role\n AND ur.granted_by_persona IS NULL\n AND (ur.read_scope <> 'all'\n OR g.write_rank > CASE ur.write_scope WHEN 'all' THEN 2 WHEN 'self' THEN 1 ELSE 0 END)\n RETURNING ur.user_id, ur.role\n ), granted AS (\n INSERT INTO lt_user_roles (user_id, role, type, read_scope, write_scope, granted_by_persona)\n SELECT g.user_id, g.role, 'member', 'all',\n CASE g.write_rank WHEN 2 THEN 'all' WHEN 1 THEN 'self' ELSE 'none' END,\n g.sustaining_persona\n FROM grants g\n WHERE NOT EXISTS (SELECT 1 FROM lt_user_roles ur WHERE ur.user_id = g.user_id AND ur.role = g.role)\n RETURNING user_id, role\n )\n SELECT\n (SELECT COUNT(*)::int FROM granted) AS granted,\n (SELECT COUNT(*)::int FROM refreshed) AS refreshed,\n (SELECT COUNT(*)::int FROM raised) AS raised,\n (SELECT COUNT(*)::int FROM removed) AS removed";
20
+ export declare const GET_USER_PERSONAS = "SELECT p.id, p.key, p.title, p.description, r.roles, up.created_at AS assigned_at\n FROM lt_user_personas up\n JOIN lt_personas p ON p.id = up.persona_id\n LEFT JOIN LATERAL (\n SELECT COALESCE(json_agg(json_build_object('role', pr.role, 'relationship', pr.relationship)\n ORDER BY pr.created_at, pr.role), '[]'::json) AS roles\n FROM lt_persona_roles pr WHERE pr.persona_id = p.id\n ) r ON true\n WHERE up.user_id = $1\n ORDER BY p.key";
21
+ /** The composed role/scope map: every membership with its sustaining persona key (null = direct). */
22
+ export declare const GET_USER_COMPOSED_ROLES = "SELECT ur.role, ur.read_scope, ur.write_scope, p.key AS granted_by_persona\n FROM lt_user_roles ur\n LEFT JOIN lt_personas p ON p.id = ur.granted_by_persona\n WHERE ur.user_id = $1\n ORDER BY ur.role";
23
+ export declare const GET_PERSONA_ID_BY_KEY = "SELECT id FROM lt_personas WHERE key = $1";
24
+ export declare const ENSURE_ROLES_EXIST = "INSERT INTO lt_roles (role) SELECT DISTINCT unnest($1::text[]) ON CONFLICT DO NOTHING";
@@ -0,0 +1,158 @@
1
+ "use strict";
2
+ // ─── Persona CRUD ─────────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.ENSURE_ROLES_EXIST = exports.GET_PERSONA_ID_BY_KEY = exports.GET_USER_COMPOSED_ROLES = exports.GET_USER_PERSONAS = exports.RECOMPUTE_PERSONA_MEMBERSHIPS = exports.LIST_SUSTAINED_USER_IDS = exports.LIST_PERSONA_HOLDER_IDS = exports.DELETE_USER_PERSONA = exports.INSERT_USER_PERSONA = exports.SYNC_PERSONA_ROLES = exports.DELETE_PERSONA_ROLE = exports.UPSERT_PERSONA_ROLE = exports.GET_PERSONA_ASSIGNEES = exports.DELETE_PERSONA = exports.UPDATE_PERSONA = exports.UPSERT_PERSONA = exports.CREATE_PERSONA = exports.GET_PERSONA_BY_KEY = exports.LIST_PERSONAS = void 0;
5
+ /** Aggregated role links for one persona, as a JSON array column. */
6
+ const ROLES_LATERAL = `LEFT JOIN LATERAL (
7
+ SELECT COALESCE(json_agg(json_build_object('role', pr.role, 'relationship', pr.relationship)
8
+ ORDER BY pr.created_at, pr.role), '[]'::json) AS roles
9
+ FROM lt_persona_roles pr WHERE pr.persona_id = p.id
10
+ ) r ON true
11
+ LEFT JOIN LATERAL (
12
+ SELECT COUNT(*)::int AS user_count FROM lt_user_personas up WHERE up.persona_id = p.id
13
+ ) u ON true`;
14
+ exports.LIST_PERSONAS = `SELECT p.id, p.key, p.title, p.description, r.roles, u.user_count, p.created_at, p.updated_at
15
+ FROM lt_personas p
16
+ ${ROLES_LATERAL}
17
+ ORDER BY p.key`;
18
+ exports.GET_PERSONA_BY_KEY = `SELECT p.id, p.key, p.title, p.description, r.roles, u.user_count, p.created_at, p.updated_at
19
+ FROM lt_personas p
20
+ ${ROLES_LATERAL}
21
+ WHERE p.key = $1`;
22
+ exports.CREATE_PERSONA = `INSERT INTO lt_personas (key, title, description) VALUES ($1, $2, $3)
23
+ RETURNING id, key, title, description, created_at, updated_at`;
24
+ /** Declarative upsert for the seed pass — the spec is authoritative for title/description. */
25
+ exports.UPSERT_PERSONA = `INSERT INTO lt_personas (key, title, description) VALUES ($1, $2, $3)
26
+ ON CONFLICT (key) DO UPDATE SET
27
+ title = EXCLUDED.title,
28
+ description = EXCLUDED.description,
29
+ updated_at = NOW()
30
+ RETURNING id`;
31
+ /** PATCH semantics: $2/$4 are set-sentinels gating whether $3/$5 write. */
32
+ exports.UPDATE_PERSONA = `UPDATE lt_personas SET
33
+ title = CASE WHEN $2 THEN $3 ELSE title END,
34
+ description = CASE WHEN $4 THEN $5 ELSE description END,
35
+ updated_at = NOW()
36
+ WHERE key = $1
37
+ RETURNING id, key, title, description, created_at, updated_at`;
38
+ exports.DELETE_PERSONA = 'DELETE FROM lt_personas WHERE id = $1';
39
+ exports.GET_PERSONA_ASSIGNEES = `SELECT us.id, us.external_id, us.display_name, us.email, up.created_at AS assigned_at
40
+ FROM lt_user_personas up
41
+ JOIN lt_users us ON us.id = up.user_id
42
+ WHERE up.persona_id = $1
43
+ ORDER BY up.created_at, us.external_id`;
44
+ // ─── Role links ───────────────────────────────────────────────────────────────
45
+ exports.UPSERT_PERSONA_ROLE = `INSERT INTO lt_persona_roles (persona_id, role, relationship) VALUES ($1, $2, $3)
46
+ ON CONFLICT (persona_id, role) DO UPDATE SET relationship = EXCLUDED.relationship
47
+ RETURNING role, relationship`;
48
+ exports.DELETE_PERSONA_ROLE = 'DELETE FROM lt_persona_roles WHERE persona_id = $1 AND role = $2';
49
+ /** Replace a persona's links declaratively: prune links not in $2, upsert the rest. */
50
+ exports.SYNC_PERSONA_ROLES = `WITH incoming AS (
51
+ SELECT x.role, x.relationship
52
+ FROM unnest($2::text[], $3::text[]) AS x(role, relationship)
53
+ ), pruned AS (
54
+ DELETE FROM lt_persona_roles pr
55
+ WHERE pr.persona_id = $1 AND pr.role NOT IN (SELECT role FROM incoming)
56
+ )
57
+ INSERT INTO lt_persona_roles (persona_id, role, relationship)
58
+ SELECT $1, role, relationship FROM incoming
59
+ ON CONFLICT (persona_id, role) DO UPDATE SET relationship = EXCLUDED.relationship`;
60
+ // ─── Assignment ───────────────────────────────────────────────────────────────
61
+ exports.INSERT_USER_PERSONA = `INSERT INTO lt_user_personas (user_id, persona_id) VALUES ($1, $2)
62
+ ON CONFLICT DO NOTHING`;
63
+ exports.DELETE_USER_PERSONA = 'DELETE FROM lt_user_personas WHERE user_id = $1 AND persona_id = $2';
64
+ exports.LIST_PERSONA_HOLDER_IDS = 'SELECT user_id FROM lt_user_personas WHERE persona_id = $1';
65
+ /** Users with a membership row still sustained by this persona (drift guard for delete). */
66
+ exports.LIST_SUSTAINED_USER_IDS = 'SELECT DISTINCT user_id FROM lt_user_roles WHERE granted_by_persona = $1';
67
+ // ─── Recompute: materialize persona grants into lt_user_roles ────────────────
68
+ //
69
+ // The single reconciliation statement behind assign, unassign, link edits,
70
+ // persona deletion, and seeding. For every user in $1::uuid[], it derives the
71
+ // highest-allowance union of the user's currently-held personas and reconciles
72
+ // lt_user_roles against it:
73
+ //
74
+ // removed — persona-sustained rows no held persona still grants
75
+ // refreshed — persona-sustained rows overlaid fresh (union scope, re-homed
76
+ // provenance so unassigning one persona never drops a role a
77
+ // sibling persona still grants)
78
+ // raised — DIRECT rows lifted to at least the union (highest allowance
79
+ // wins); scope is never lowered, type and provenance untouched
80
+ // granted — missing rows inserted as scoped members
81
+ //
82
+ // Direct rows (granted_by_persona IS NULL) are never deleted here — persona
83
+ // removal must not touch hand-tuned memberships. Write-rank encoding:
84
+ // write-all=2 (all), write-self=1 (self), read-all=0 (none).
85
+ exports.RECOMPUTE_PERSONA_MEMBERSHIPS = `WITH affected AS (
86
+ SELECT unnest($1::uuid[]) AS user_id
87
+ ), grants AS (
88
+ SELECT up.user_id, pr.role,
89
+ MAX(CASE pr.relationship WHEN 'write-all' THEN 2 WHEN 'write-self' THEN 1 ELSE 0 END) AS write_rank,
90
+ (MIN(pr.persona_id::text))::uuid AS sustaining_persona
91
+ FROM lt_user_personas up
92
+ JOIN lt_persona_roles pr ON pr.persona_id = up.persona_id
93
+ JOIN affected a ON a.user_id = up.user_id
94
+ GROUP BY up.user_id, pr.role
95
+ ), removed AS (
96
+ DELETE FROM lt_user_roles ur
97
+ USING affected a
98
+ WHERE ur.user_id = a.user_id
99
+ AND ur.granted_by_persona IS NOT NULL
100
+ AND NOT EXISTS (SELECT 1 FROM grants g WHERE g.user_id = ur.user_id AND g.role = ur.role)
101
+ RETURNING ur.user_id, ur.role
102
+ ), refreshed AS (
103
+ UPDATE lt_user_roles ur
104
+ SET read_scope = 'all',
105
+ write_scope = CASE g.write_rank WHEN 2 THEN 'all' WHEN 1 THEN 'self' ELSE 'none' END,
106
+ granted_by_persona = g.sustaining_persona
107
+ FROM grants g
108
+ WHERE ur.user_id = g.user_id AND ur.role = g.role
109
+ AND ur.granted_by_persona IS NOT NULL
110
+ RETURNING ur.user_id, ur.role
111
+ ), raised AS (
112
+ UPDATE lt_user_roles ur
113
+ SET read_scope = 'all',
114
+ write_scope = CASE
115
+ WHEN g.write_rank = 2 THEN 'all'
116
+ WHEN g.write_rank = 1 AND ur.write_scope = 'none' THEN 'self'
117
+ ELSE ur.write_scope
118
+ END
119
+ FROM grants g
120
+ WHERE ur.user_id = g.user_id AND ur.role = g.role
121
+ AND ur.granted_by_persona IS NULL
122
+ AND (ur.read_scope <> 'all'
123
+ OR g.write_rank > CASE ur.write_scope WHEN 'all' THEN 2 WHEN 'self' THEN 1 ELSE 0 END)
124
+ RETURNING ur.user_id, ur.role
125
+ ), granted AS (
126
+ INSERT INTO lt_user_roles (user_id, role, type, read_scope, write_scope, granted_by_persona)
127
+ SELECT g.user_id, g.role, 'member', 'all',
128
+ CASE g.write_rank WHEN 2 THEN 'all' WHEN 1 THEN 'self' ELSE 'none' END,
129
+ g.sustaining_persona
130
+ FROM grants g
131
+ WHERE NOT EXISTS (SELECT 1 FROM lt_user_roles ur WHERE ur.user_id = g.user_id AND ur.role = g.role)
132
+ RETURNING user_id, role
133
+ )
134
+ SELECT
135
+ (SELECT COUNT(*)::int FROM granted) AS granted,
136
+ (SELECT COUNT(*)::int FROM refreshed) AS refreshed,
137
+ (SELECT COUNT(*)::int FROM raised) AS raised,
138
+ (SELECT COUNT(*)::int FROM removed) AS removed`;
139
+ // ─── forUser ──────────────────────────────────────────────────────────────────
140
+ exports.GET_USER_PERSONAS = `SELECT p.id, p.key, p.title, p.description, r.roles, up.created_at AS assigned_at
141
+ FROM lt_user_personas up
142
+ JOIN lt_personas p ON p.id = up.persona_id
143
+ LEFT JOIN LATERAL (
144
+ SELECT COALESCE(json_agg(json_build_object('role', pr.role, 'relationship', pr.relationship)
145
+ ORDER BY pr.created_at, pr.role), '[]'::json) AS roles
146
+ FROM lt_persona_roles pr WHERE pr.persona_id = p.id
147
+ ) r ON true
148
+ WHERE up.user_id = $1
149
+ ORDER BY p.key`;
150
+ /** The composed role/scope map: every membership with its sustaining persona key (null = direct). */
151
+ exports.GET_USER_COMPOSED_ROLES = `SELECT ur.role, ur.read_scope, ur.write_scope, p.key AS granted_by_persona
152
+ FROM lt_user_roles ur
153
+ LEFT JOIN lt_personas p ON p.id = ur.granted_by_persona
154
+ WHERE ur.user_id = $1
155
+ ORDER BY ur.role`;
156
+ // ─── Shared ───────────────────────────────────────────────────────────────────
157
+ exports.GET_PERSONA_ID_BY_KEY = 'SELECT id FROM lt_personas WHERE key = $1';
158
+ exports.ENSURE_ROLES_EXIST = `INSERT INTO lt_roles (role) SELECT DISTINCT unnest($1::text[]) ON CONFLICT DO NOTHING`;