@hotmeshio/long-tail 0.10.2 → 0.10.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/build/api/bot-accounts.d.ts +4 -0
- package/build/api/bot-accounts.js +3 -1
- package/build/api/escalations/metadata.js +21 -1
- package/build/api/escalations/resolve.d.ts +8 -0
- package/build/api/escalations/resolve.js +82 -3
- package/build/api/roles.d.ts +2 -0
- package/build/api/roles.js +4 -0
- package/build/bin/ltc.js +10 -1
- package/build/index.js +7 -1
- package/build/lib/cli/client.d.ts +4 -1
- package/build/lib/cli/client.js +4 -2
- package/build/lib/db/schemas/023_role_enforce_schema.sql +18 -0
- package/build/lib/typed-resolution.d.ts +35 -0
- package/build/lib/typed-resolution.js +37 -0
- package/build/modules/branding.d.ts +21 -2
- package/build/modules/branding.js +22 -1
- package/build/modules/config.d.ts +1 -0
- package/build/modules/config.js +5 -0
- package/build/routes/bot-accounts.js +3 -1
- package/build/routes/settings.js +21 -0
- package/build/sdk/index.d.ts +4 -0
- package/build/sdk/index.js +14 -0
- package/build/services/escalation/crud.d.ts +18 -6
- package/build/services/escalation/crud.js +18 -2
- package/build/services/escalation/index.d.ts +1 -0
- package/build/services/escalation/index.js +1 -0
- package/build/services/escalation/resolver-validation.d.ts +28 -0
- package/build/services/escalation/resolver-validation.js +95 -0
- package/build/services/escalation/sql.d.ts +12 -2
- package/build/services/escalation/sql.js +22 -2
- package/build/services/iam/bots.d.ts +4 -1
- package/build/services/iam/bots.js +5 -3
- package/build/services/iam/sql.d.ts +2 -2
- package/build/services/iam/sql.js +15 -1
- package/build/services/role/enforcement-cache.d.ts +21 -0
- package/build/services/role/enforcement-cache.js +0 -0
- package/build/services/role/index.js +5 -0
- package/build/services/role/sql.d.ts +12 -2
- package/build/services/role/sql.js +15 -2
- package/build/services/role/types.d.ts +9 -0
- package/build/shared/form-validation/derive-field-label.d.ts +14 -0
- package/build/shared/form-validation/derive-field-label.js +31 -0
- package/build/shared/form-validation/field-validator.d.ts +45 -0
- package/build/shared/form-validation/field-validator.js +214 -0
- package/build/shared/form-validation/index.d.ts +12 -0
- package/build/shared/form-validation/index.js +28 -0
- package/build/shared/form-validation/validate-resolver-payload.d.ts +41 -0
- package/build/shared/form-validation/validate-resolver-payload.js +62 -0
- package/build/shared/form-validation/x-lt-bind.d.ts +41 -0
- package/build/shared/form-validation/x-lt-bind.js +110 -0
- package/build/shared/form-validation/x-lt-help.d.ts +21 -0
- package/build/shared/form-validation/x-lt-help.js +91 -0
- package/build/shared/form-validation/x-lt-show-if.d.ts +7 -0
- package/build/shared/form-validation/x-lt-show-if.js +84 -0
- package/build/system/mcp-servers/admin/schemas.d.ts +3 -0
- package/build/system/mcp-servers/admin/schemas.js +1 -0
- package/build/system/mcp-servers/admin/users.js +3 -1
- package/build/system/mcp-servers/human-queue-schemas.d.ts +4 -4
- package/build/system/mcp-servers/human-queue-schemas.js +4 -1
- package/build/system/mcp-servers/human-queue.js +41 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/sdk.d.ts +6 -1
- package/build/types/startup.d.ts +26 -0
- package/build/types/validation.d.ts +43 -0
- package/build/types/validation.js +28 -0
- package/dashboard/dist/assets/{AdminDashboard-Dy031e9g.js → AdminDashboard-Cma2NGsF.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-Dy031e9g.js.map → AdminDashboard-Cma2NGsF.js.map} +1 -1
- package/dashboard/dist/assets/AgentConfigPage-CbIhM-fa.js +16 -0
- package/dashboard/dist/assets/AgentConfigPage-CbIhM-fa.js.map +1 -0
- package/dashboard/dist/assets/AgentDetailPage-DuLEKzRT.js +4 -0
- package/dashboard/dist/assets/AgentDetailPage-DuLEKzRT.js.map +1 -0
- package/dashboard/dist/assets/AgentsPage-DZO9i7Nl.js +2 -0
- package/dashboard/dist/assets/AgentsPage-DZO9i7Nl.js.map +1 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-DlVoNd8B.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-DlVoNd8B.js.map +1 -0
- package/dashboard/dist/assets/{BotPicker-2QEIpc-K.js → BotPicker-CidlGr6f.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-2QEIpc-K.js.map → BotPicker-CidlGr6f.js.map} +1 -1
- package/dashboard/dist/assets/CapabilitiesPage-BMNNTa0D.js +2 -0
- package/dashboard/dist/assets/{CapabilitiesPage-lQJGZ_-t.js.map → CapabilitiesPage-BMNNTa0D.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-CHWP0WI6.js → CollapsibleSection-W9F9lziT.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-CHWP0WI6.js.map → CollapsibleSection-W9F9lziT.js.map} +1 -1
- package/dashboard/dist/assets/ConfirmDeleteModal-BUh-Dzba.js +2 -0
- package/dashboard/dist/assets/{ConfirmDeleteModal-C0x0HcHX.js.map → ConfirmDeleteModal-BUh-Dzba.js.map} +1 -1
- package/dashboard/dist/assets/{CopyableId-uot2mE_g.js → CopyableId-B1mxKfVb.js} +2 -2
- package/dashboard/dist/assets/{CopyableId-uot2mE_g.js.map → CopyableId-B1mxKfVb.js.map} +1 -1
- package/dashboard/dist/assets/CountdownTimer-lkv_7msa.js +2 -0
- package/dashboard/dist/assets/{CountdownTimer-TXsRlTU4.js.map → CountdownTimer-lkv_7msa.js.map} +1 -1
- package/dashboard/dist/assets/{CredentialsPage-BKxS-1sA.js → CredentialsPage-DyOgfnPB.js} +2 -2
- package/dashboard/dist/assets/CredentialsPage-DyOgfnPB.js.map +1 -0
- package/dashboard/dist/assets/{CronLabel-Bd2G0Rap.js → CronLabel-geum95dj.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-Bd2G0Rap.js.map → CronLabel-geum95dj.js.map} +1 -1
- package/dashboard/dist/assets/CustomDurationPicker-ClufO2ew.js +2 -0
- package/dashboard/dist/assets/CustomDurationPicker-ClufO2ew.js.map +1 -0
- package/dashboard/dist/assets/DataTable-BTu2AaEl.js +2 -0
- package/dashboard/dist/assets/DataTable-BTu2AaEl.js.map +1 -0
- package/dashboard/dist/assets/{DropZone-DNDEgMpJ.js → DropZone-C3hOjJtC.js} +2 -2
- package/dashboard/dist/assets/{DropZone-DNDEgMpJ.js.map → DropZone-C3hOjJtC.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-g7GO7f8L.js → ElapsedCell-DFOW9T4b.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-g7GO7f8L.js.map → ElapsedCell-DFOW9T4b.js.map} +1 -1
- package/dashboard/dist/assets/EscalationListSchemaPage-_IhqXIQu.js +20 -0
- package/dashboard/dist/assets/EscalationListSchemaPage-_IhqXIQu.js.map +1 -0
- package/dashboard/dist/assets/EscalationSchemaPage-BCC7iscN.js +9 -0
- package/dashboard/dist/assets/EscalationSchemaPage-BCC7iscN.js.map +1 -0
- package/dashboard/dist/assets/EscalationsOverview-CvIdOtoP.js +2 -0
- package/dashboard/dist/assets/EscalationsOverview-CvIdOtoP.js.map +1 -0
- package/dashboard/dist/assets/EventTable-DJWJ7J0m.js +2 -0
- package/dashboard/dist/assets/EventTable-DJWJ7J0m.js.map +1 -0
- package/dashboard/dist/assets/FilterBar-BR3ztZ8y.js +2 -0
- package/dashboard/dist/assets/FilterBar-BR3ztZ8y.js.map +1 -0
- package/dashboard/dist/assets/GraphInvokePage-Cwn-8Glr.js +2 -0
- package/dashboard/dist/assets/GraphInvokePage-Cwn-8Glr.js.map +1 -0
- package/dashboard/dist/assets/HomePage-D8RVntg0.js +2 -0
- package/dashboard/dist/assets/HomePage-D8RVntg0.js.map +1 -0
- package/dashboard/dist/assets/ListToolbar-DuoEdVtK.js +2 -0
- package/dashboard/dist/assets/ListToolbar-DuoEdVtK.js.map +1 -0
- package/dashboard/dist/assets/McpOverview-NXKsGiWU.js +2 -0
- package/dashboard/dist/assets/McpOverview-NXKsGiWU.js.map +1 -0
- package/dashboard/dist/assets/McpQueryDetailPage-BC-iXfIx.js +5 -0
- package/dashboard/dist/assets/McpQueryDetailPage-BC-iXfIx.js.map +1 -0
- package/dashboard/dist/assets/{McpQueryPage-CsKNXUhb.js → McpQueryPage-BPJmFYOY.js} +2 -2
- package/dashboard/dist/assets/{McpQueryPage-CsKNXUhb.js.map → McpQueryPage-BPJmFYOY.js.map} +1 -1
- package/dashboard/dist/assets/McpRunDetailPage-B49nUXEl.js +2 -0
- package/dashboard/dist/assets/McpRunDetailPage-B49nUXEl.js.map +1 -0
- package/dashboard/dist/assets/McpRunsPage-Ce_XIYpP.js +2 -0
- package/dashboard/dist/assets/McpRunsPage-Ce_XIYpP.js.map +1 -0
- package/dashboard/dist/assets/NamespacePill-LWvZXt_g.js +2 -0
- package/dashboard/dist/assets/NamespacePill-LWvZXt_g.js.map +1 -0
- package/dashboard/dist/assets/OperationsPage-DI7AQO9f.js +2 -0
- package/dashboard/dist/assets/OperationsPage-DI7AQO9f.js.map +1 -0
- package/dashboard/dist/assets/OperatorDashboard-DpXjhs-3.js +2 -0
- package/dashboard/dist/assets/{OperatorDashboard-_axS17fp.js.map → OperatorDashboard-DpXjhs-3.js.map} +1 -1
- package/dashboard/dist/assets/PageHeader-DvEmMMP6.js +2 -0
- package/dashboard/dist/assets/PageHeader-DvEmMMP6.js.map +1 -0
- package/dashboard/dist/assets/PageHeaderWithStats-X0eAmu-x.js +2 -0
- package/dashboard/dist/assets/PageHeaderWithStats-X0eAmu-x.js.map +1 -0
- package/dashboard/dist/assets/{PriorityBadge-B4ykBH7f.js → PriorityBadge-CCERr9vQ.js} +2 -2
- package/dashboard/dist/assets/PriorityBadge-CCERr9vQ.js.map +1 -0
- package/dashboard/dist/assets/ProcessDetailPage-DpYSMRj1.js +2 -0
- package/dashboard/dist/assets/ProcessDetailPage-DpYSMRj1.js.map +1 -0
- package/dashboard/dist/assets/{ProcessesListPage-CoQEi_Su.js → ProcessesListPage-CQ9tSw7U.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-CoQEi_Su.js.map → ProcessesListPage-CQ9tSw7U.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-DKeQKDkW.js +8 -0
- package/dashboard/dist/assets/RoleDetailPage-DKeQKDkW.js.map +1 -0
- package/dashboard/dist/assets/RolePill-CWhoLwef.js +2 -0
- package/dashboard/dist/assets/RolePill-CWhoLwef.js.map +1 -0
- package/dashboard/dist/assets/RolesPage-D-6kIact.js +2 -0
- package/dashboard/dist/assets/RolesPage-D-6kIact.js.map +1 -0
- package/dashboard/dist/assets/RunAsSelector-DzYcpQTm.js +2 -0
- package/dashboard/dist/assets/RunAsSelector-DzYcpQTm.js.map +1 -0
- package/dashboard/dist/assets/StatCard-DBANWIss.js +2 -0
- package/dashboard/dist/assets/StatCard-DBANWIss.js.map +1 -0
- package/dashboard/dist/assets/StatusBadge-BbqghFMi.js +2 -0
- package/dashboard/dist/assets/StatusBadge-BbqghFMi.js.map +1 -0
- package/dashboard/dist/assets/{StickyPagination-Cd83AHd_.js → StickyPagination-BTGfmGV3.js} +2 -2
- package/dashboard/dist/assets/{StickyPagination-Cd83AHd_.js.map → StickyPagination-BTGfmGV3.js.map} +1 -1
- package/dashboard/dist/assets/StreamMessageDetail-DqRpzA_6.js +2 -0
- package/dashboard/dist/assets/StreamMessageDetail-DqRpzA_6.js.map +1 -0
- package/dashboard/dist/assets/SwimlaneTimeline-CA106TYo.js +2 -0
- package/dashboard/dist/assets/SwimlaneTimeline-CA106TYo.js.map +1 -0
- package/dashboard/dist/assets/TagInput-CoR4MzsW.js +2 -0
- package/dashboard/dist/assets/TagInput-CoR4MzsW.js.map +1 -0
- package/dashboard/dist/assets/{TaskDetailPage-DnjhW7qv.js → TaskDetailPage-BRg-w2Ev.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-DnjhW7qv.js.map → TaskDetailPage-BRg-w2Ev.js.map} +1 -1
- package/dashboard/dist/assets/TaskQueuePill-Dw3YN8M-.js +2 -0
- package/dashboard/dist/assets/TaskQueuePill-Dw3YN8M-.js.map +1 -0
- package/dashboard/dist/assets/{TasksListPage-DySfZFgs.js → TasksListPage-kB_j9G8o.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-DySfZFgs.js.map → TasksListPage-kB_j9G8o.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-BKOl-7Tt.js → TimeAgo-D7dSIY7-.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-BKOl-7Tt.js.map → TimeAgo-D7dSIY7-.js.map} +1 -1
- package/dashboard/dist/assets/TimestampCell-BZe6bx18.js +2 -0
- package/dashboard/dist/assets/TimestampCell-BZe6bx18.js.map +1 -0
- package/dashboard/dist/assets/ToolPill-xX-buQtY.js +2 -0
- package/dashboard/dist/assets/ToolPill-xX-buQtY.js.map +1 -0
- package/dashboard/dist/assets/ToolTestPanel-U3NaF_T_.js +2 -0
- package/dashboard/dist/assets/ToolTestPanel-U3NaF_T_.js.map +1 -0
- package/dashboard/dist/assets/TopicDetailPage-BpALvXP1.js +9 -0
- package/dashboard/dist/assets/TopicDetailPage-BpALvXP1.js.map +1 -0
- package/dashboard/dist/assets/TopicsPage-Cy3chaca.js +2 -0
- package/dashboard/dist/assets/{TopicsPage-5yW63ELl.js.map → TopicsPage-Cy3chaca.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-DcXhOSVm.js → UserName-VXM1oHMC.js} +2 -2
- package/dashboard/dist/assets/{UserName-DcXhOSVm.js.map → UserName-VXM1oHMC.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowExecutionPage-BCq2cR1V.js +2 -0
- package/dashboard/dist/assets/WorkflowExecutionPage-BCq2cR1V.js.map +1 -0
- package/dashboard/dist/assets/WorkflowPill-CQ1AmhlC.js +2 -0
- package/dashboard/dist/assets/WorkflowPill-CQ1AmhlC.js.map +1 -0
- package/dashboard/dist/assets/{WorkflowsDashboard-C0Bbrd5M.js → WorkflowsDashboard-BbM69MnB.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsDashboard-C0Bbrd5M.js.map → WorkflowsDashboard-BbM69MnB.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowsOverview-D5HthpAW.js +2 -0
- package/dashboard/dist/assets/WorkflowsOverview-D5HthpAW.js.map +1 -0
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-BcbrYhDI.js → YamlWorkflowDetailPage-CDDVMYWi.js} +10 -10
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-BcbrYhDI.js.map → YamlWorkflowDetailPage-CDDVMYWi.js.map} +1 -1
- package/dashboard/dist/assets/YamlWorkflowsPage-FFjWwydj.js +2 -0
- package/dashboard/dist/assets/YamlWorkflowsPage-FFjWwydj.js.map +1 -0
- package/dashboard/dist/assets/{agents-CopQ3t5o.js → agents-BwiSNu73.js} +2 -2
- package/dashboard/dist/assets/{agents-CopQ3t5o.js.map → agents-BwiSNu73.js.map} +1 -1
- package/dashboard/dist/assets/bots-ClgSrxel.js +2 -0
- package/dashboard/dist/assets/bots-ClgSrxel.js.map +1 -0
- package/dashboard/dist/assets/{capabilities-5JRieYUy.js → capabilities-JUaKsby4.js} +2 -2
- package/dashboard/dist/assets/{capabilities-5JRieYUy.js.map → capabilities-JUaKsby4.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-B4rT6tIw.js → controlplane-CuTQ7eUE.js} +2 -2
- package/dashboard/dist/assets/{controlplane-B4rT6tIw.js.map → controlplane-CuTQ7eUE.js.map} +1 -1
- package/dashboard/dist/assets/escalation-columns-CS0c7cAr.js +2 -0
- package/dashboard/dist/assets/escalation-columns-CS0c7cAr.js.map +1 -0
- package/dashboard/dist/assets/index-BBA4AxmL.js +2 -0
- package/dashboard/dist/assets/index-BBA4AxmL.js.map +1 -0
- package/dashboard/dist/assets/index-C-8xXyvr.js +63 -0
- package/dashboard/dist/assets/index-C-8xXyvr.js.map +1 -0
- package/dashboard/dist/assets/index-C1t0YIXC.js +5 -0
- package/dashboard/dist/assets/index-C1t0YIXC.js.map +1 -0
- package/dashboard/dist/assets/index-C9o2UNhC.js +2 -0
- package/dashboard/dist/assets/index-C9o2UNhC.js.map +1 -0
- package/dashboard/dist/assets/index-CBj-vstO.js +2 -0
- package/dashboard/dist/assets/{index-Dd1RxGNw.js.map → index-CBj-vstO.js.map} +1 -1
- package/dashboard/dist/assets/index-CUguKMXZ.js +2 -0
- package/dashboard/dist/assets/index-CUguKMXZ.js.map +1 -0
- package/dashboard/dist/assets/index-Cavwd8UU.js +6 -0
- package/dashboard/dist/assets/index-Cavwd8UU.js.map +1 -0
- package/dashboard/dist/assets/index-CnsPBTXL.js +2 -0
- package/dashboard/dist/assets/index-CnsPBTXL.js.map +1 -0
- package/dashboard/dist/assets/index-CoPWHo_H.js +2 -0
- package/dashboard/dist/assets/index-CoPWHo_H.js.map +1 -0
- package/dashboard/dist/assets/index-CwQVaa7x.js +2 -0
- package/dashboard/dist/assets/index-CwQVaa7x.js.map +1 -0
- package/dashboard/dist/assets/index-DSa-6Ux8.js +2 -0
- package/dashboard/dist/assets/index-DSa-6Ux8.js.map +1 -0
- package/dashboard/dist/assets/index-DeMs_aBT.css +1 -0
- package/dashboard/dist/assets/index-Ikj_NTuy.js +5 -0
- package/dashboard/dist/assets/index-Ikj_NTuy.js.map +1 -0
- package/dashboard/dist/assets/index-Xd1jcHe7.js +2 -0
- package/dashboard/dist/assets/index-Xd1jcHe7.js.map +1 -0
- package/dashboard/dist/assets/{knowledge-B9bmoXOe.js → knowledge-BHBt-6kW.js} +2 -2
- package/dashboard/dist/assets/{knowledge-B9bmoXOe.js.map → knowledge-BHBt-6kW.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-DYMFBOFz.js → mcp-CBcz4BCS.js} +2 -2
- package/dashboard/dist/assets/{mcp-DYMFBOFz.js.map → mcp-CBcz4BCS.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-wloSpyPM.js → mcp-query-C4fKTOYo.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-wloSpyPM.js.map → mcp-query-C4fKTOYo.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-DBXX6TEi.js → pipelines-CmAAH3B6.js} +2 -2
- package/dashboard/dist/assets/{pipelines-DBXX6TEi.js.map → pipelines-CmAAH3B6.js.map} +1 -1
- package/dashboard/dist/assets/role-display-Dvo2lDpU.js +2 -0
- package/dashboard/dist/assets/role-display-Dvo2lDpU.js.map +1 -0
- package/dashboard/dist/assets/task-queues-7-sHKPsN.js +2 -0
- package/dashboard/dist/assets/task-queues-7-sHKPsN.js.map +1 -0
- package/dashboard/dist/assets/{tasks-CD3-C7tP.js → tasks-BAEsqcz2.js} +2 -2
- package/dashboard/dist/assets/{tasks-CD3-C7tP.js.map → tasks-BAEsqcz2.js.map} +1 -1
- package/dashboard/dist/assets/{topics-D8pKi6zA.js → topics-CY0pJ6_o.js} +2 -2
- package/dashboard/dist/assets/{topics-D8pKi6zA.js.map → topics-CY0pJ6_o.js.map} +1 -1
- package/dashboard/dist/assets/{useEventHooks-DV9pLDOW.js → useEventHooks-BlpwRvHO.js} +2 -2
- package/dashboard/dist/assets/{useEventHooks-DV9pLDOW.js.map → useEventHooks-BlpwRvHO.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-DykYhZY9.js → useNamespace-CoNUlZ6Z.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-DykYhZY9.js.map → useNamespace-CoNUlZ6Z.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-By044UAv.js → useYamlActivityEvents-8GUn5Wie.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-By044UAv.js.map → useYamlActivityEvents-8GUn5Wie.js.map} +1 -1
- package/dashboard/dist/assets/{users-mI9goDjj.js → users-uGtDRGxR.js} +2 -2
- package/dashboard/dist/assets/{users-mI9goDjj.js.map → users-uGtDRGxR.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons-ihWGa7_d.js → vendor-icons-DKCAtTJ9.js} +135 -130
- package/dashboard/dist/assets/vendor-icons-DKCAtTJ9.js.map +1 -0
- package/dashboard/dist/assets/{workflows-DCUilz68.js → workflows-DoX-AtHN.js} +2 -2
- package/dashboard/dist/assets/{workflows-DCUilz68.js.map → workflows-DoX-AtHN.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-DWNMP1PT.js → yaml-workflows-DM3jVem2.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-DWNMP1PT.js.map → yaml-workflows-DM3jVem2.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +15 -1
- package/docs/api/http/roles.md +2 -0
- package/docs/design-principles.md +234 -0
- package/docs/hitl/design.md +228 -0
- package/docs/hitl/form.md +21 -1
- package/docs/hitl/iframe.md +1 -1
- package/docs/hitl/x-lt-display.md +56 -0
- package/docs/hitl/x-lt-layout.md +20 -1
- package/docs/hitl/x-lt-list-schema.md +8 -1
- package/docs/hitl/x-lt-widget.md +23 -0
- package/docs/hitl-guide.md +4 -2
- package/docs/schema-enforcement.md +149 -0
- package/package.json +1 -1
- package/dashboard/dist/assets/AgentConfigPage-CqmtYBbC.js +0 -16
- package/dashboard/dist/assets/AgentConfigPage-CqmtYBbC.js.map +0 -1
- package/dashboard/dist/assets/AgentDetailPage-oYLBrN74.js +0 -4
- package/dashboard/dist/assets/AgentDetailPage-oYLBrN74.js.map +0 -1
- package/dashboard/dist/assets/AgentsPage-CHczRx5N.js +0 -2
- package/dashboard/dist/assets/AgentsPage-CHczRx5N.js.map +0 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-DDVuivEE.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-DDVuivEE.js.map +0 -1
- package/dashboard/dist/assets/CapabilitiesPage-lQJGZ_-t.js +0 -2
- package/dashboard/dist/assets/ConfirmDeleteModal-C0x0HcHX.js +0 -2
- package/dashboard/dist/assets/CountdownTimer-TXsRlTU4.js +0 -2
- package/dashboard/dist/assets/CredentialsPage-BKxS-1sA.js.map +0 -1
- package/dashboard/dist/assets/CustomDurationPicker-j4bBrDJ6.js +0 -2
- package/dashboard/dist/assets/CustomDurationPicker-j4bBrDJ6.js.map +0 -1
- package/dashboard/dist/assets/DataTable-Byn3oyT9.js +0 -2
- package/dashboard/dist/assets/DataTable-Byn3oyT9.js.map +0 -1
- package/dashboard/dist/assets/EscalationListSchemaPage-CIJ-OgVC.js +0 -20
- package/dashboard/dist/assets/EscalationListSchemaPage-CIJ-OgVC.js.map +0 -1
- package/dashboard/dist/assets/EscalationSchemaPage-C7dHgG8e.js +0 -9
- package/dashboard/dist/assets/EscalationSchemaPage-C7dHgG8e.js.map +0 -1
- package/dashboard/dist/assets/EscalationsOverview-D8NrATg2.js +0 -2
- package/dashboard/dist/assets/EscalationsOverview-D8NrATg2.js.map +0 -1
- package/dashboard/dist/assets/EventTable-DBV1P2ME.js +0 -2
- package/dashboard/dist/assets/EventTable-DBV1P2ME.js.map +0 -1
- package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js +0 -2
- package/dashboard/dist/assets/FilterBar-B5Vk5tBF.js.map +0 -1
- package/dashboard/dist/assets/GraphInvokePage-DbHvggQ_.js +0 -2
- package/dashboard/dist/assets/GraphInvokePage-DbHvggQ_.js.map +0 -1
- package/dashboard/dist/assets/HomePage-BpexnDO_.js +0 -2
- package/dashboard/dist/assets/HomePage-BpexnDO_.js.map +0 -1
- package/dashboard/dist/assets/ListToolbar-KC6LGtKP.js +0 -2
- package/dashboard/dist/assets/ListToolbar-KC6LGtKP.js.map +0 -1
- package/dashboard/dist/assets/McpOverview-BDZC9Hvl.js +0 -2
- package/dashboard/dist/assets/McpOverview-BDZC9Hvl.js.map +0 -1
- package/dashboard/dist/assets/McpQueryDetailPage-CXhgX1pS.js +0 -5
- package/dashboard/dist/assets/McpQueryDetailPage-CXhgX1pS.js.map +0 -1
- package/dashboard/dist/assets/McpRunDetailPage-BM3mUSlH.js +0 -2
- package/dashboard/dist/assets/McpRunDetailPage-BM3mUSlH.js.map +0 -1
- package/dashboard/dist/assets/McpRunsPage-f5hDbpLo.js +0 -2
- package/dashboard/dist/assets/McpRunsPage-f5hDbpLo.js.map +0 -1
- package/dashboard/dist/assets/NamespacePill-C4u6Go1W.js +0 -2
- package/dashboard/dist/assets/NamespacePill-C4u6Go1W.js.map +0 -1
- package/dashboard/dist/assets/OperationsPage-D37cZ8Qh.js +0 -2
- package/dashboard/dist/assets/OperationsPage-D37cZ8Qh.js.map +0 -1
- package/dashboard/dist/assets/OperatorDashboard-_axS17fp.js +0 -2
- package/dashboard/dist/assets/PageHeader-Bxa1XFz0.js +0 -2
- package/dashboard/dist/assets/PageHeader-Bxa1XFz0.js.map +0 -1
- package/dashboard/dist/assets/PageHeaderWithStats-D_sO0MqJ.js +0 -2
- package/dashboard/dist/assets/PageHeaderWithStats-D_sO0MqJ.js.map +0 -1
- package/dashboard/dist/assets/PriorityBadge-B4ykBH7f.js.map +0 -1
- package/dashboard/dist/assets/ProcessDetailPage-D8pAWUXx.js +0 -2
- package/dashboard/dist/assets/ProcessDetailPage-D8pAWUXx.js.map +0 -1
- package/dashboard/dist/assets/RoleDetailPage-ufbypNNR.js +0 -8
- package/dashboard/dist/assets/RoleDetailPage-ufbypNNR.js.map +0 -1
- package/dashboard/dist/assets/RolePill-DLwmVMam.js +0 -2
- package/dashboard/dist/assets/RolePill-DLwmVMam.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-CxpXm4Sw.js +0 -2
- package/dashboard/dist/assets/RolesPage-CxpXm4Sw.js.map +0 -1
- package/dashboard/dist/assets/RunAsSelector-BebF8Lib.js +0 -2
- package/dashboard/dist/assets/RunAsSelector-BebF8Lib.js.map +0 -1
- package/dashboard/dist/assets/SlidePanel-BDVDuwIf.js +0 -2
- package/dashboard/dist/assets/SlidePanel-BDVDuwIf.js.map +0 -1
- package/dashboard/dist/assets/StatCard-DrdlUeQz.js +0 -2
- package/dashboard/dist/assets/StatCard-DrdlUeQz.js.map +0 -1
- package/dashboard/dist/assets/StatusBadge-CEQMRmSQ.js +0 -2
- package/dashboard/dist/assets/StatusBadge-CEQMRmSQ.js.map +0 -1
- package/dashboard/dist/assets/StreamMessageDetail-BIXbrZM0.js +0 -2
- package/dashboard/dist/assets/StreamMessageDetail-BIXbrZM0.js.map +0 -1
- package/dashboard/dist/assets/SwimlaneTimeline-CC5qZjYo.js +0 -2
- package/dashboard/dist/assets/SwimlaneTimeline-CC5qZjYo.js.map +0 -1
- package/dashboard/dist/assets/TagInput-C2gi47M8.js +0 -2
- package/dashboard/dist/assets/TagInput-C2gi47M8.js.map +0 -1
- package/dashboard/dist/assets/TaskQueuePill-BPP0Almh.js +0 -2
- package/dashboard/dist/assets/TaskQueuePill-BPP0Almh.js.map +0 -1
- package/dashboard/dist/assets/TimestampCell-DGOsba-A.js +0 -2
- package/dashboard/dist/assets/TimestampCell-DGOsba-A.js.map +0 -1
- package/dashboard/dist/assets/ToolPill-D74TkYB-.js +0 -2
- package/dashboard/dist/assets/ToolPill-D74TkYB-.js.map +0 -1
- package/dashboard/dist/assets/ToolTestPanel-Bez6OyRL.js +0 -2
- package/dashboard/dist/assets/ToolTestPanel-Bez6OyRL.js.map +0 -1
- package/dashboard/dist/assets/TopicDetailPage-BHccgkU_.js +0 -9
- package/dashboard/dist/assets/TopicDetailPage-BHccgkU_.js.map +0 -1
- package/dashboard/dist/assets/TopicsPage-5yW63ELl.js +0 -2
- package/dashboard/dist/assets/WorkflowExecutionPage-CDDLled5.js +0 -2
- package/dashboard/dist/assets/WorkflowExecutionPage-CDDLled5.js.map +0 -1
- package/dashboard/dist/assets/WorkflowPill-Fjc2q9U9.js +0 -2
- package/dashboard/dist/assets/WorkflowPill-Fjc2q9U9.js.map +0 -1
- package/dashboard/dist/assets/WorkflowsOverview-D8MkSMex.js +0 -2
- package/dashboard/dist/assets/WorkflowsOverview-D8MkSMex.js.map +0 -1
- package/dashboard/dist/assets/YamlWorkflowsPage-9Ki54d-9.js +0 -2
- package/dashboard/dist/assets/YamlWorkflowsPage-9Ki54d-9.js.map +0 -1
- package/dashboard/dist/assets/bots-CAYJYL9E.js +0 -2
- package/dashboard/dist/assets/bots-CAYJYL9E.js.map +0 -1
- package/dashboard/dist/assets/escalation-columns-BbS5X61l.js +0 -2
- package/dashboard/dist/assets/escalation-columns-BbS5X61l.js.map +0 -1
- package/dashboard/dist/assets/index-B68A3v-S.js +0 -2
- package/dashboard/dist/assets/index-B68A3v-S.js.map +0 -1
- package/dashboard/dist/assets/index-BAi89Xpm.js +0 -2
- package/dashboard/dist/assets/index-BAi89Xpm.js.map +0 -1
- package/dashboard/dist/assets/index-BJ0jjFsI.js +0 -5
- package/dashboard/dist/assets/index-BJ0jjFsI.js.map +0 -1
- package/dashboard/dist/assets/index-BOJMUk4F.js +0 -2
- package/dashboard/dist/assets/index-BOJMUk4F.js.map +0 -1
- package/dashboard/dist/assets/index-BrgYykPV.js +0 -2
- package/dashboard/dist/assets/index-BrgYykPV.js.map +0 -1
- package/dashboard/dist/assets/index-BxgtIqvl.js +0 -2
- package/dashboard/dist/assets/index-BxgtIqvl.js.map +0 -1
- package/dashboard/dist/assets/index-C0cHXaX-.css +0 -1
- package/dashboard/dist/assets/index-CDHDoHYz.js +0 -6
- package/dashboard/dist/assets/index-CDHDoHYz.js.map +0 -1
- package/dashboard/dist/assets/index-CUZJHXew.js +0 -2
- package/dashboard/dist/assets/index-CUZJHXew.js.map +0 -1
- package/dashboard/dist/assets/index-D5n_e59T.js +0 -2
- package/dashboard/dist/assets/index-D5n_e59T.js.map +0 -1
- package/dashboard/dist/assets/index-DIgiIoou.js +0 -63
- package/dashboard/dist/assets/index-DIgiIoou.js.map +0 -1
- package/dashboard/dist/assets/index-Dd1RxGNw.js +0 -2
- package/dashboard/dist/assets/index-iOb3sGsC.js +0 -5
- package/dashboard/dist/assets/index-iOb3sGsC.js.map +0 -1
- package/dashboard/dist/assets/index-mcAkQblr.js +0 -2
- package/dashboard/dist/assets/index-mcAkQblr.js.map +0 -1
- package/dashboard/dist/assets/vendor-icons-ihWGa7_d.js.map +0 -1
|
@@ -191,12 +191,16 @@ export declare function claimByMetadata(key: string, value: string, userId: stri
|
|
|
191
191
|
}) | null>;
|
|
192
192
|
export interface ResolveByMetadataResult {
|
|
193
193
|
/**
|
|
194
|
-
* 'resolved'
|
|
195
|
-
* 'signal_required'
|
|
196
|
-
* 'conflict'
|
|
197
|
-
* 'not_found'
|
|
194
|
+
* 'resolved' = done atomically in SQL (no signal backing).
|
|
195
|
+
* 'signal_required' = signal backing present, caller must deliver the signal.
|
|
196
|
+
* 'conflict' = signal_id row already claimed by a concurrent caller; skip re-signal.
|
|
197
|
+
* 'not_found' = no pending escalation matched the metadata filter.
|
|
198
|
+
* 'validation_required' = the target's role enforces its schema; NOTHING was
|
|
199
|
+
* written. The caller validates the payload against
|
|
200
|
+
* `row`, then re-invokes with assertId = row.id (and
|
|
201
|
+
* no enforcing set) to claim + resolve.
|
|
198
202
|
*/
|
|
199
|
-
outcome: 'resolved' | 'signal_required' | 'conflict' | 'not_found';
|
|
203
|
+
outcome: 'resolved' | 'signal_required' | 'conflict' | 'not_found' | 'validation_required';
|
|
200
204
|
/** The resolved escalation (when outcome = 'resolved') */
|
|
201
205
|
escalation?: LTEscalationRecord;
|
|
202
206
|
/** Legacy conditionLT signal info (when signalId is set, caller uses handle.signal) */
|
|
@@ -207,6 +211,14 @@ export interface ResolveByMetadataResult {
|
|
|
207
211
|
workflowId?: string;
|
|
208
212
|
workflowType?: string;
|
|
209
213
|
taskQueue?: string;
|
|
214
|
+
/** The unmodified target row fields the schema gate validates against (when outcome = 'validation_required') */
|
|
215
|
+
row?: {
|
|
216
|
+
id: string;
|
|
217
|
+
role: string | null;
|
|
218
|
+
metadata: Record<string, any> | null;
|
|
219
|
+
envelope: string | null;
|
|
220
|
+
escalation_payload: string | null;
|
|
221
|
+
};
|
|
210
222
|
}
|
|
211
223
|
/**
|
|
212
224
|
* Atomic resolve by metadata with signal guard, in a single CTE.
|
|
@@ -218,4 +230,4 @@ export interface ResolveByMetadataResult {
|
|
|
218
230
|
* business logic over the shared table, so it runs as one atomic statement on
|
|
219
231
|
* `hmsh_escalations` rather than through the generic SDK resolve.
|
|
220
232
|
*/
|
|
221
|
-
export declare function resolveByMetadataAtomic(key: string, value: string, userId: string, resolverPayload: Record<string, any>, metadata?: Record<string, any>, writeAllRoles?: string[] | null, writeSelfRoles?: string[] | null): Promise<ResolveByMetadataResult>;
|
|
233
|
+
export declare function resolveByMetadataAtomic(key: string, value: string, userId: string, resolverPayload: Record<string, any>, metadata?: Record<string, any>, writeAllRoles?: string[] | null, writeSelfRoles?: string[] | null, enforcingRoles?: string[] | null, assertId?: string | null): Promise<ResolveByMetadataResult>;
|
|
@@ -512,7 +512,7 @@ async function claimByMetadata(key, value, userId, durationMinutes = 30, metadat
|
|
|
512
512
|
* business logic over the shared table, so it runs as one atomic statement on
|
|
513
513
|
* `hmsh_escalations` rather than through the generic SDK resolve.
|
|
514
514
|
*/
|
|
515
|
-
async function resolveByMetadataAtomic(key, value, userId, resolverPayload, metadata, writeAllRoles, writeSelfRoles) {
|
|
515
|
+
async function resolveByMetadataAtomic(key, value, userId, resolverPayload, metadata, writeAllRoles, writeSelfRoles, enforcingRoles, assertId) {
|
|
516
516
|
await (0, client_1.ensureEscalationCompatView)();
|
|
517
517
|
const pool = (0, db_1.getPool)();
|
|
518
518
|
const filter = JSON.stringify({ [key]: value });
|
|
@@ -522,10 +522,26 @@ async function resolveByMetadataAtomic(key, value, userId, resolverPayload, meta
|
|
|
522
522
|
// (possibly empty); the write_self branch matches only rows assigned to userId.
|
|
523
523
|
const allRoles = writeAllRoles ?? null;
|
|
524
524
|
const selfRoles = writeSelfRoles ?? null;
|
|
525
|
-
const { rows } = await pool.query(sql_1.RESOLVE_BY_METADATA_ATOMIC, [filter, userId, payloadJson, metaPatch, allRoles, selfRoles
|
|
525
|
+
const { rows } = await pool.query(sql_1.RESOLVE_BY_METADATA_ATOMIC, [filter, userId, payloadJson, metaPatch, allRoles, selfRoles,
|
|
526
|
+
enforcingRoles?.length ? enforcingRoles : null, assertId ?? null]);
|
|
526
527
|
if (rows.length === 0)
|
|
527
528
|
return { outcome: 'not_found' };
|
|
528
529
|
const row = rows[0];
|
|
530
|
+
// Enforcing-role target — nothing written; hand the row's validation surface
|
|
531
|
+
// back so the API layer can run the schema gate and re-invoke with assertId.
|
|
532
|
+
if (row.outcome === 'validation_required') {
|
|
533
|
+
return {
|
|
534
|
+
outcome: 'validation_required',
|
|
535
|
+
escalationId: row.target_id,
|
|
536
|
+
row: {
|
|
537
|
+
id: row.target_id,
|
|
538
|
+
role: row.target_role ?? null,
|
|
539
|
+
metadata: row.target_metadata ?? null,
|
|
540
|
+
envelope: row.target_envelope ?? null,
|
|
541
|
+
escalation_payload: row.target_escalation_payload ?? null,
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
|
529
545
|
if (row.outcome === 'resolved') {
|
|
530
546
|
const escalation = (0, map_1.toEscalationRecord)(row);
|
|
531
547
|
publishEscalationChange({
|
|
@@ -2,5 +2,6 @@ export * from './types';
|
|
|
2
2
|
export * from './crud';
|
|
3
3
|
export * from './bulk';
|
|
4
4
|
export * from './queries';
|
|
5
|
+
export * from './resolver-validation';
|
|
5
6
|
export { ensureEscalationCompatView } from './client';
|
|
6
7
|
export { ensureFacetReady, searchByFacets, searchGroups, countByFacets, claimGroups, claimByFacets, } from './facets';
|
|
@@ -19,6 +19,7 @@ __exportStar(require("./types"), exports);
|
|
|
19
19
|
__exportStar(require("./crud"), exports);
|
|
20
20
|
__exportStar(require("./bulk"), exports);
|
|
21
21
|
__exportStar(require("./queries"), exports);
|
|
22
|
+
__exportStar(require("./resolver-validation"), exports);
|
|
22
23
|
var client_1 = require("./client");
|
|
23
24
|
Object.defineProperty(exports, "ensureEscalationCompatView", { enumerable: true, get: function () { return client_1.ensureEscalationCompatView; } });
|
|
24
25
|
var facets_1 = require("./facets");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type LTFieldViolation, type LTValidationErrorBody } from '../../types/validation';
|
|
2
|
+
/** The row fields the gate reads — every resolve surface already holds these. */
|
|
3
|
+
export interface ResolvableEscalationRow {
|
|
4
|
+
id: string;
|
|
5
|
+
role: string | null;
|
|
6
|
+
metadata?: Record<string, any> | null;
|
|
7
|
+
envelope?: string | null;
|
|
8
|
+
escalation_payload?: string | null;
|
|
9
|
+
[key: string]: any;
|
|
10
|
+
}
|
|
11
|
+
/** A failed check: which role's contract rejected, at which version, and why. */
|
|
12
|
+
export interface ResolverSchemaViolationReport {
|
|
13
|
+
role: string;
|
|
14
|
+
schemaVersion: number | null;
|
|
15
|
+
violations: LTFieldViolation[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Validate a submitted resolverPayload against the escalation's enforced form
|
|
19
|
+
* schema. Returns null when the resolution may proceed (role does not enforce,
|
|
20
|
+
* no schema to enforce, or the payload passes); otherwise the violation report.
|
|
21
|
+
*
|
|
22
|
+
* `loadEnvelope` lets a caller supply a richer envelope read (e.g. the by-id
|
|
23
|
+
* surface's task fallback); by default the row's own envelope column is used.
|
|
24
|
+
* It is only invoked when the role actually enforces.
|
|
25
|
+
*/
|
|
26
|
+
export declare function checkResolverPayload(escalation: ResolvableEscalationRow, resolverPayload: Record<string, any>, loadEnvelope?: () => Promise<Record<string, any>>): Promise<ResolverSchemaViolationReport | null>;
|
|
27
|
+
/** The canonical 422 body for a violation report (types/validation.ts). */
|
|
28
|
+
export declare function toValidationErrorBody(report: ResolverSchemaViolationReport): LTValidationErrorBody;
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkResolverPayload = checkResolverPayload;
|
|
4
|
+
exports.toValidationErrorBody = toValidationErrorBody;
|
|
5
|
+
/**
|
|
6
|
+
* Server-side resolver schema enforcement — the API-first counterpart of the
|
|
7
|
+
* dashboard's pre-submission pass. Every resolve surface (HTTP, SDK, MCP)
|
|
8
|
+
* calls checkResolverPayload before committing a resolution; roles that opt in
|
|
9
|
+
* via enforce_schema get their form_schema enforced as an API contract, and
|
|
10
|
+
* violations surface as the canonical 422 body (types/validation.ts).
|
|
11
|
+
*
|
|
12
|
+
* Production shape: the gate consults the cached enforcing-role set first, so
|
|
13
|
+
* when no involved role enforces, a resolve costs zero additional SQL. Schema
|
|
14
|
+
* reads for enforcing roles come from the enforcement cache (immutable pinned
|
|
15
|
+
* snapshots cached indefinitely; latest schemas under a short TTL).
|
|
16
|
+
*
|
|
17
|
+
* Schema resolution matches the resolve UI, most specific first:
|
|
18
|
+
* 1. metadata.form_schema — a full form embedded on the row
|
|
19
|
+
* 2. the lt_role_schemas snapshot pinned by metadata.schema_version
|
|
20
|
+
* 3. the role's live (latest) form_schema
|
|
21
|
+
* A role that enforces but declares no schema has no contract to enforce —
|
|
22
|
+
* the gate passes.
|
|
23
|
+
*/
|
|
24
|
+
const form_validation_1 = require("../../shared/form-validation");
|
|
25
|
+
const enforcement_cache_1 = require("../role/enforcement-cache");
|
|
26
|
+
const escalation_1 = require("../../types/escalation");
|
|
27
|
+
const validation_1 = require("../../types/validation");
|
|
28
|
+
function parseJsonOrNull(value) {
|
|
29
|
+
if (!value)
|
|
30
|
+
return null;
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(value);
|
|
33
|
+
return typeof parsed === 'object' && parsed !== null ? parsed : null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** The row's schema-version pin as an integer, mirroring the SQL's guarded cast. */
|
|
40
|
+
function pinnedVersionOf(metadata) {
|
|
41
|
+
const raw = metadata[escalation_1.ESCALATION_METADATA_KEYS.SCHEMA_VERSION];
|
|
42
|
+
if (typeof raw === 'number' && Number.isInteger(raw) && raw >= 1)
|
|
43
|
+
return raw;
|
|
44
|
+
if (typeof raw === 'string' && /^[0-9]+$/.test(raw))
|
|
45
|
+
return parseInt(raw, 10);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Validate a submitted resolverPayload against the escalation's enforced form
|
|
50
|
+
* schema. Returns null when the resolution may proceed (role does not enforce,
|
|
51
|
+
* no schema to enforce, or the payload passes); otherwise the violation report.
|
|
52
|
+
*
|
|
53
|
+
* `loadEnvelope` lets a caller supply a richer envelope read (e.g. the by-id
|
|
54
|
+
* surface's task fallback); by default the row's own envelope column is used.
|
|
55
|
+
* It is only invoked when the role actually enforces.
|
|
56
|
+
*/
|
|
57
|
+
async function checkResolverPayload(escalation, resolverPayload, loadEnvelope) {
|
|
58
|
+
const role = escalation.role;
|
|
59
|
+
if (!role)
|
|
60
|
+
return null;
|
|
61
|
+
const enforcing = await (0, enforcement_cache_1.getEnforcingRoles)();
|
|
62
|
+
if (!enforcing.has(role))
|
|
63
|
+
return null;
|
|
64
|
+
const metadata = (escalation.metadata ?? {});
|
|
65
|
+
const pin = pinnedVersionOf(metadata);
|
|
66
|
+
const embedded = metadata[escalation_1.ESCALATION_METADATA_KEYS.FORM_SCHEMA];
|
|
67
|
+
const schema = embedded && typeof embedded === 'object'
|
|
68
|
+
? embedded
|
|
69
|
+
: await (0, enforcement_cache_1.getEnforcedFormSchema)(role, pin);
|
|
70
|
+
if (!schema)
|
|
71
|
+
return null;
|
|
72
|
+
const envelope = loadEnvelope
|
|
73
|
+
? await loadEnvelope()
|
|
74
|
+
: parseJsonOrNull(escalation.envelope) ?? {};
|
|
75
|
+
const violations = (0, form_validation_1.validateResolverPayload)(schema, resolverPayload, {
|
|
76
|
+
escalation: escalation,
|
|
77
|
+
metadata,
|
|
78
|
+
envelope,
|
|
79
|
+
payload: parseJsonOrNull(escalation.escalation_payload),
|
|
80
|
+
});
|
|
81
|
+
if (violations.length === 0)
|
|
82
|
+
return null;
|
|
83
|
+
return { role, schemaVersion: pin, violations };
|
|
84
|
+
}
|
|
85
|
+
/** The canonical 422 body for a violation report (types/validation.ts). */
|
|
86
|
+
function toValidationErrorBody(report) {
|
|
87
|
+
const n = report.violations.length;
|
|
88
|
+
return {
|
|
89
|
+
error: `resolverPayload failed schema validation (${n} violation${n === 1 ? '' : 's'})`,
|
|
90
|
+
code: validation_1.LT_ERROR_CODES.SCHEMA_VALIDATION,
|
|
91
|
+
violations: report.violations,
|
|
92
|
+
role: report.role,
|
|
93
|
+
schemaVersion: report.schemaVersion,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -36,14 +36,24 @@ export declare function searchEscalationsQuery(orderBy: string): string;
|
|
|
36
36
|
*
|
|
37
37
|
* $1 = metadata filter (jsonb), $2 = userId, $3 = resolver_payload (jsonb),
|
|
38
38
|
* $4 = metadata patch (jsonb, nullable), $5 = write_all roles (text[], null = global /
|
|
39
|
-
* no filter), $6 = write_self roles (text[], nullable)
|
|
39
|
+
* no filter), $6 = write_self roles (text[], nullable), $7 = enforcing roles
|
|
40
|
+
* (text[], null = none), $8 = assert id (uuid, nullable).
|
|
40
41
|
*
|
|
41
42
|
* Write-scope is folded into the same FOR UPDATE statement (no TOCTOU): the row
|
|
42
43
|
* is resolvable if the caller has global access ($5 NULL), or the row's role is in
|
|
43
44
|
* their write_all set, or the row's role is in their write_self set AND it is
|
|
44
45
|
* already assigned to them ($2). assigned_to is indexed, so the self-branch scales.
|
|
46
|
+
*
|
|
47
|
+
* Schema enforcement rides as a fifth outcome: when the target's role is in $7,
|
|
48
|
+
* NOTHING is written — the statement returns 'validation_required' with the
|
|
49
|
+
* row's role/metadata/envelope so the API layer can validate the payload, then
|
|
50
|
+
* re-invoke with $8 = the validated row's id and $7 = NULL. The asserted second
|
|
51
|
+
* pass claims + resolves exactly as a first pass would (state re-asserted:
|
|
52
|
+
* status must still be pending and the id must still match, so a concurrent
|
|
53
|
+
* resolution surfaces as zero rows → the caller's conflict handling). This is
|
|
54
|
+
* the same pick-then-guarded-finish shape the signal outcomes already use.
|
|
45
55
|
*/
|
|
46
|
-
export declare const RESOLVE_BY_METADATA_ATOMIC = "WITH target AS MATERIALIZED (\n SELECT *\n FROM public.hmsh_escalations\n WHERE metadata @> $1::jsonb\n AND status = 'pending'\n AND (\n $5::text[] IS NULL\n OR role = ANY($5)\n OR (role = ANY($6::text[]) AND assigned_to = $2)\n )\n ORDER BY priority ASC, created_at ASC\n LIMIT 1\n FOR UPDATE\n),\nclaimed AS (\n UPDATE public.hmsh_escalations e\n SET assigned_to = COALESCE(e.assigned_to, $2),\n claimed_at = COALESCE(e.claimed_at, NOW()),\n assigned_until = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.assigned_until\n ELSE NOW() + INTERVAL '5 minutes' END,\n claim_expires_at = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.claim_expires_at\n ELSE NOW() + INTERVAL '5 minutes' END,\n metadata = CASE WHEN $4::jsonb IS NOT NULL\n THEN COALESCE(e.metadata, '{}'::jsonb) || $4::jsonb\n ELSE e.metadata END,\n updated_at = NOW()\n FROM target\n WHERE e.id = target.id\n AND target.signal_key IS NULL\n RETURNING e.*\n),\nresolved AS (\n UPDATE public.hmsh_escalations e\n SET status = 'resolved',\n resolved_at = NOW(),\n resolver_payload = $3,\n updated_at = NOW()\n FROM claimed\n WHERE e.id = claimed.id\n AND (claimed.metadata->>'signal_id') IS NULL\n RETURNING e.*\n)\nSELECT\n resolved.*,\n target.id AS target_id,\n target.metadata->>'signal_id' AS signal_id,\n target.signal_key AS signal_key,\n target.workflow_id AS target_workflow_id,\n target.workflow_type AS target_workflow_type,\n target.task_queue AS target_task_queue,\n (target.assigned_to IS NOT NULL\n AND target.assigned_until IS NOT NULL\n AND target.assigned_until > NOW()) AS signal_already_claimed,\n CASE
|
|
56
|
+
export declare const RESOLVE_BY_METADATA_ATOMIC = "WITH target AS MATERIALIZED (\n SELECT *\n FROM public.hmsh_escalations\n WHERE metadata @> $1::jsonb\n AND status = 'pending'\n AND (\n $5::text[] IS NULL\n OR role = ANY($5)\n OR (role = ANY($6::text[]) AND assigned_to = $2)\n )\n AND ($8::uuid IS NULL OR id = $8::uuid)\n ORDER BY priority ASC, created_at ASC\n LIMIT 1\n FOR UPDATE\n),\nclaimed AS (\n UPDATE public.hmsh_escalations e\n SET assigned_to = COALESCE(e.assigned_to, $2),\n claimed_at = COALESCE(e.claimed_at, NOW()),\n assigned_until = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.assigned_until\n ELSE NOW() + INTERVAL '5 minutes' END,\n claim_expires_at = CASE\n WHEN e.assigned_to IS NOT NULL AND e.assigned_until > NOW() THEN e.claim_expires_at\n ELSE NOW() + INTERVAL '5 minutes' END,\n metadata = CASE WHEN $4::jsonb IS NOT NULL\n THEN COALESCE(e.metadata, '{}'::jsonb) || $4::jsonb\n ELSE e.metadata END,\n updated_at = NOW()\n FROM target\n WHERE e.id = target.id\n AND target.signal_key IS NULL\n AND ($7::text[] IS NULL OR NOT (target.role = ANY($7)))\n RETURNING e.*\n),\nresolved AS (\n UPDATE public.hmsh_escalations e\n SET status = 'resolved',\n resolved_at = NOW(),\n resolver_payload = $3,\n updated_at = NOW()\n FROM claimed\n WHERE e.id = claimed.id\n AND (claimed.metadata->>'signal_id') IS NULL\n RETURNING e.*\n)\nSELECT\n resolved.*,\n target.id AS target_id,\n target.role AS target_role,\n target.metadata AS target_metadata,\n target.envelope AS target_envelope,\n target.escalation_payload AS target_escalation_payload,\n target.metadata->>'signal_id' AS signal_id,\n target.signal_key AS signal_key,\n target.workflow_id AS target_workflow_id,\n target.workflow_type AS target_workflow_type,\n target.task_queue AS target_task_queue,\n (target.assigned_to IS NOT NULL\n AND target.assigned_until IS NOT NULL\n AND target.assigned_until > NOW()) AS signal_already_claimed,\n CASE\n WHEN resolved.id IS NOT NULL THEN 'resolved'\n WHEN $7::text[] IS NOT NULL AND target.role = ANY($7) THEN 'validation_required'\n ELSE 'signal_required'\n END AS outcome\nFROM target\nLEFT JOIN resolved ON resolved.id = target.id";
|
|
47
57
|
/**
|
|
48
58
|
* Live counts — pending, active claims, and priority (past-threshold) per role.
|
|
49
59
|
*
|
|
@@ -118,12 +118,22 @@ function searchEscalationsQuery(orderBy) {
|
|
|
118
118
|
*
|
|
119
119
|
* $1 = metadata filter (jsonb), $2 = userId, $3 = resolver_payload (jsonb),
|
|
120
120
|
* $4 = metadata patch (jsonb, nullable), $5 = write_all roles (text[], null = global /
|
|
121
|
-
* no filter), $6 = write_self roles (text[], nullable)
|
|
121
|
+
* no filter), $6 = write_self roles (text[], nullable), $7 = enforcing roles
|
|
122
|
+
* (text[], null = none), $8 = assert id (uuid, nullable).
|
|
122
123
|
*
|
|
123
124
|
* Write-scope is folded into the same FOR UPDATE statement (no TOCTOU): the row
|
|
124
125
|
* is resolvable if the caller has global access ($5 NULL), or the row's role is in
|
|
125
126
|
* their write_all set, or the row's role is in their write_self set AND it is
|
|
126
127
|
* already assigned to them ($2). assigned_to is indexed, so the self-branch scales.
|
|
128
|
+
*
|
|
129
|
+
* Schema enforcement rides as a fifth outcome: when the target's role is in $7,
|
|
130
|
+
* NOTHING is written — the statement returns 'validation_required' with the
|
|
131
|
+
* row's role/metadata/envelope so the API layer can validate the payload, then
|
|
132
|
+
* re-invoke with $8 = the validated row's id and $7 = NULL. The asserted second
|
|
133
|
+
* pass claims + resolves exactly as a first pass would (state re-asserted:
|
|
134
|
+
* status must still be pending and the id must still match, so a concurrent
|
|
135
|
+
* resolution surfaces as zero rows → the caller's conflict handling). This is
|
|
136
|
+
* the same pick-then-guarded-finish shape the signal outcomes already use.
|
|
127
137
|
*/
|
|
128
138
|
exports.RESOLVE_BY_METADATA_ATOMIC = `\
|
|
129
139
|
WITH target AS MATERIALIZED (
|
|
@@ -136,6 +146,7 @@ WITH target AS MATERIALIZED (
|
|
|
136
146
|
OR role = ANY($5)
|
|
137
147
|
OR (role = ANY($6::text[]) AND assigned_to = $2)
|
|
138
148
|
)
|
|
149
|
+
AND ($8::uuid IS NULL OR id = $8::uuid)
|
|
139
150
|
ORDER BY priority ASC, created_at ASC
|
|
140
151
|
LIMIT 1
|
|
141
152
|
FOR UPDATE
|
|
@@ -157,6 +168,7 @@ claimed AS (
|
|
|
157
168
|
FROM target
|
|
158
169
|
WHERE e.id = target.id
|
|
159
170
|
AND target.signal_key IS NULL
|
|
171
|
+
AND ($7::text[] IS NULL OR NOT (target.role = ANY($7)))
|
|
160
172
|
RETURNING e.*
|
|
161
173
|
),
|
|
162
174
|
resolved AS (
|
|
@@ -173,6 +185,10 @@ resolved AS (
|
|
|
173
185
|
SELECT
|
|
174
186
|
resolved.*,
|
|
175
187
|
target.id AS target_id,
|
|
188
|
+
target.role AS target_role,
|
|
189
|
+
target.metadata AS target_metadata,
|
|
190
|
+
target.envelope AS target_envelope,
|
|
191
|
+
target.escalation_payload AS target_escalation_payload,
|
|
176
192
|
target.metadata->>'signal_id' AS signal_id,
|
|
177
193
|
target.signal_key AS signal_key,
|
|
178
194
|
target.workflow_id AS target_workflow_id,
|
|
@@ -181,7 +197,11 @@ SELECT
|
|
|
181
197
|
(target.assigned_to IS NOT NULL
|
|
182
198
|
AND target.assigned_until IS NOT NULL
|
|
183
199
|
AND target.assigned_until > NOW()) AS signal_already_claimed,
|
|
184
|
-
CASE
|
|
200
|
+
CASE
|
|
201
|
+
WHEN resolved.id IS NOT NULL THEN 'resolved'
|
|
202
|
+
WHEN $7::text[] IS NOT NULL AND target.role = ANY($7) THEN 'validation_required'
|
|
203
|
+
ELSE 'signal_required'
|
|
204
|
+
END AS outcome
|
|
185
205
|
FROM target
|
|
186
206
|
LEFT JOIN resolved ON resolved.id = target.id`;
|
|
187
207
|
// ---------------------------------------------------------------------------
|
|
@@ -37,7 +37,10 @@ export declare function getBot(id: string): Promise<BotAccountRecord | null>;
|
|
|
37
37
|
/**
|
|
38
38
|
* List all bot accounts with pagination.
|
|
39
39
|
*/
|
|
40
|
-
export declare function listBots(limit?: number, offset?: number
|
|
40
|
+
export declare function listBots(limit?: number, offset?: number, filters?: {
|
|
41
|
+
status?: string;
|
|
42
|
+
search?: string;
|
|
43
|
+
}): Promise<{
|
|
41
44
|
bots: BotAccountRecord[];
|
|
42
45
|
total: number;
|
|
43
46
|
}>;
|
|
@@ -58,11 +58,13 @@ async function getBot(id) {
|
|
|
58
58
|
/**
|
|
59
59
|
* List all bot accounts with pagination.
|
|
60
60
|
*/
|
|
61
|
-
async function listBots(limit = 50, offset = 0) {
|
|
61
|
+
async function listBots(limit = 50, offset = 0, filters = {}) {
|
|
62
62
|
const pool = await (0, db_1.getPool)();
|
|
63
|
+
const status = filters.status || null;
|
|
64
|
+
const search = filters.search || null;
|
|
63
65
|
const [{ rows }, countResult] = await Promise.all([
|
|
64
|
-
pool.query(sql_1.LIST_BOTS, [limit, offset]),
|
|
65
|
-
pool.query(sql_1.COUNT_BOTS),
|
|
66
|
+
pool.query(sql_1.LIST_BOTS, [limit, offset, status, search]),
|
|
67
|
+
pool.query(sql_1.COUNT_BOTS, [status, search]),
|
|
66
68
|
]);
|
|
67
69
|
// Attach roles to each bot
|
|
68
70
|
const bots = await Promise.all(rows.map(async (row) => {
|
|
@@ -3,8 +3,8 @@ export declare const INSERT_EPHEMERAL: (expiresExpr: string) => string;
|
|
|
3
3
|
export declare const EXCHANGE_EPHEMERAL = "\n UPDATE lt_ephemeral_credentials\n SET use_count = use_count + 1\n WHERE token = $1\n AND (expires_at IS NULL OR expires_at > NOW())\n AND (max_uses = 0 OR use_count < max_uses)\n RETURNING value, use_count, max_uses";
|
|
4
4
|
export declare const DELETE_EPHEMERAL = "\n DELETE FROM lt_ephemeral_credentials WHERE token = $1";
|
|
5
5
|
export declare const CLEANUP_EXPIRED_EPHEMERAL = "\n DELETE FROM lt_ephemeral_credentials\n WHERE expires_at IS NOT NULL AND expires_at < NOW()";
|
|
6
|
-
export declare const LIST_BOTS = "\n SELECT * FROM lt_users\n WHERE account_type = 'bot'\n ORDER BY created_at DESC\n LIMIT $1 OFFSET $2";
|
|
7
|
-
export declare const COUNT_BOTS = "\n SELECT COUNT(*)::int AS total FROM lt_users
|
|
6
|
+
export declare const LIST_BOTS = "\n SELECT * FROM lt_users\n WHERE account_type = 'bot'\n AND ($3::text IS NULL OR status = $3)\n AND ($4::text IS NULL\n OR display_name ILIKE '%' || $4 || '%'\n OR external_id ILIKE '%' || $4 || '%'\n OR metadata->>'description' ILIKE '%' || $4 || '%')\n ORDER BY created_at DESC\n LIMIT $1 OFFSET $2";
|
|
7
|
+
export declare const COUNT_BOTS = "\n SELECT COUNT(*)::int AS total FROM lt_users\n WHERE account_type = 'bot'\n AND ($1::text IS NULL OR status = $1)\n AND ($2::text IS NULL\n OR display_name ILIKE '%' || $2 || '%'\n OR external_id ILIKE '%' || $2 || '%'\n OR metadata->>'description' ILIKE '%' || $2 || '%')";
|
|
8
8
|
export declare const SET_ACCOUNT_TYPE_BOT = "\n UPDATE lt_users SET account_type = $1 WHERE id = $2";
|
|
9
9
|
export declare const GET_USER_BY_EXTERNAL_ID = "\n SELECT id FROM lt_users WHERE external_id = $1";
|
|
10
10
|
export declare const GET_USER_WITH_ROLES_FLEXIBLE = "\n SELECT u.id, u.external_id, u.display_name, u.status, u.metadata,\n r.role, r.type AS role_type\n FROM lt_users u\n LEFT JOIN lt_user_roles r ON r.user_id = u.id\n WHERE u.external_id = $1 OR u.id::text = $1\n ORDER BY r.created_at";
|
|
@@ -21,13 +21,27 @@ exports.CLEANUP_EXPIRED_EPHEMERAL = `
|
|
|
21
21
|
DELETE FROM lt_ephemeral_credentials
|
|
22
22
|
WHERE expires_at IS NOT NULL AND expires_at < NOW()`;
|
|
23
23
|
// ─── Bot accounts ───────────────────────────────────────────────────────────
|
|
24
|
+
// $3 = status (nullable), $4 = search (nullable) — free text over the bot's
|
|
25
|
+
// display fields, mirroring listUsers' search so the two Accounts tabs offer
|
|
26
|
+
// the same server-side filter surface.
|
|
24
27
|
exports.LIST_BOTS = `
|
|
25
28
|
SELECT * FROM lt_users
|
|
26
29
|
WHERE account_type = 'bot'
|
|
30
|
+
AND ($3::text IS NULL OR status = $3)
|
|
31
|
+
AND ($4::text IS NULL
|
|
32
|
+
OR display_name ILIKE '%' || $4 || '%'
|
|
33
|
+
OR external_id ILIKE '%' || $4 || '%'
|
|
34
|
+
OR metadata->>'description' ILIKE '%' || $4 || '%')
|
|
27
35
|
ORDER BY created_at DESC
|
|
28
36
|
LIMIT $1 OFFSET $2`;
|
|
29
37
|
exports.COUNT_BOTS = `
|
|
30
|
-
SELECT COUNT(*)::int AS total FROM lt_users
|
|
38
|
+
SELECT COUNT(*)::int AS total FROM lt_users
|
|
39
|
+
WHERE account_type = 'bot'
|
|
40
|
+
AND ($1::text IS NULL OR status = $1)
|
|
41
|
+
AND ($2::text IS NULL
|
|
42
|
+
OR display_name ILIKE '%' || $2 || '%'
|
|
43
|
+
OR external_id ILIKE '%' || $2 || '%'
|
|
44
|
+
OR metadata->>'description' ILIKE '%' || $2 || '%')`;
|
|
31
45
|
exports.SET_ACCOUNT_TYPE_BOT = `
|
|
32
46
|
UPDATE lt_users SET account_type = $1 WHERE id = $2`;
|
|
33
47
|
exports.GET_USER_BY_EXTERNAL_ID = `
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The set of roles with enforce_schema on, cached under the TTL. Callers check
|
|
3
|
+
* membership before doing ANY validation work — an empty set short-circuits
|
|
4
|
+
* every resolve surface to its pre-enforcement behavior.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getEnforcingRoles(): Promise<Set<string>>;
|
|
7
|
+
/** Whether this role currently enforces resolver schema validation. */
|
|
8
|
+
export declare function isEnforcingRole(role: string): Promise<boolean>;
|
|
9
|
+
/**
|
|
10
|
+
* The authoritative form schema for validating a resolve against `role`,
|
|
11
|
+
* honoring an escalation's version pin. Pinned versions come from the
|
|
12
|
+
* immutable snapshot cache; unpinned reads come from the TTL'd latest cache.
|
|
13
|
+
* Null when the role declares no schema (nothing to enforce).
|
|
14
|
+
*/
|
|
15
|
+
export declare function getEnforcedFormSchema(role: string, pinnedVersion?: number | null): Promise<Record<string, any> | null>;
|
|
16
|
+
/**
|
|
17
|
+
* Drop cached enforcement state after a role write. With a role, drops that
|
|
18
|
+
* role's latest-schema entry plus the enforcing set; without, drops
|
|
19
|
+
* everything. Pinned snapshots are immutable and never need invalidation.
|
|
20
|
+
*/
|
|
21
|
+
export declare function invalidateRoleEnforcement(role?: string): void;
|
|
Binary file
|
|
@@ -21,6 +21,7 @@ exports.deleteRole = deleteRole;
|
|
|
21
21
|
const db_1 = require("../../lib/db");
|
|
22
22
|
const user_1 = require("../user");
|
|
23
23
|
const sql_1 = require("./sql");
|
|
24
|
+
const enforcement_cache_1 = require("./enforcement-cache");
|
|
24
25
|
/**
|
|
25
26
|
* Get the roles a given source role can escalate to.
|
|
26
27
|
*/
|
|
@@ -147,9 +148,13 @@ async function updateRoleMetadata(role, input) {
|
|
|
147
148
|
provided('upstream_roles'), upstreams,
|
|
148
149
|
provided('list_schema'), input.list_schema != null ? JSON.stringify(input.list_schema) : null,
|
|
149
150
|
provided('default_pins'), input.default_pins != null ? JSON.stringify(input.default_pins) : null,
|
|
151
|
+
provided('enforce_schema'), input.enforce_schema ?? null,
|
|
150
152
|
]);
|
|
151
153
|
if (!rows[0])
|
|
152
154
|
return null;
|
|
155
|
+
// Enforcement state may have changed — drop this role's cached entries so
|
|
156
|
+
// the resolve gate sees the write immediately in this process.
|
|
157
|
+
(0, enforcement_cache_1.invalidateRoleEnforcement)(role);
|
|
153
158
|
// The statement's CTEs share one snapshot, so the returned row cannot see
|
|
154
159
|
// the upstream sync it just performed — echo the write, or read when the
|
|
155
160
|
// field wasn't touched.
|
|
@@ -42,9 +42,19 @@ export declare const DELETE_ROLE = "\n DELETE FROM lt_roles WHERE role = $1";
|
|
|
42
42
|
*
|
|
43
43
|
* default_pins ($31 = provided sentinel, $32 = value) is unversioned config:
|
|
44
44
|
* the pinned-view seeds a role hands its members.
|
|
45
|
+
*
|
|
46
|
+
* enforce_schema ($33 = provided sentinel, $34 = value) is unversioned config:
|
|
47
|
+
* the server-side resolver-validation opt-in. The service layer invalidates
|
|
48
|
+
* the enforcement cache after this statement commits.
|
|
45
49
|
*/
|
|
46
|
-
export declare const UPDATE_ROLE_METADATA = "\n WITH updated AS (\n UPDATE lt_roles SET\n title = CASE WHEN $2::boolean THEN $3 ELSE title END,\n description = CASE WHEN $4::boolean THEN $5 ELSE description END,\n form_schema = CASE WHEN $6::boolean THEN $7::jsonb ELSE form_schema END,\n metadata_schema = CASE WHEN $8::boolean THEN $9::jsonb ELSE metadata_schema END,\n properties = CASE WHEN $10::boolean THEN COALESCE($11::jsonb, '{}'::jsonb) ELSE properties END,\n ops_visible = CASE WHEN $12::boolean THEN $13::boolean ELSE ops_visible END,\n parent_role = CASE WHEN $14::boolean THEN $15 ELSE parent_role END,\n sla_minutes = CASE WHEN $16::boolean THEN $17::numeric ELSE sla_minutes END,\n target_per_hour = CASE WHEN $18::boolean THEN $19::numeric ELSE target_per_hour END,\n worker_count = CASE WHEN $20::boolean THEN $21::int ELSE worker_count END,\n priority_threshold_minutes\n = CASE WHEN $22::boolean THEN $23::numeric ELSE priority_threshold_minutes END,\n priority_facet = CASE WHEN $24::boolean THEN $25 ELSE priority_facet END,\n list_schema = CASE WHEN $29::boolean THEN $30::jsonb ELSE list_schema END,\n default_pins = CASE WHEN $31::boolean THEN $32::jsonb ELSE default_pins END,\n current_schema_version = CASE\n WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)\n OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)\n THEN COALESCE(current_schema_version, 0) + 1\n ELSE current_schema_version END,\n current_list_schema_version = CASE\n WHEN $29::boolean AND $30::jsonb IS DISTINCT FROM list_schema\n THEN COALESCE(current_list_schema_version, 0) + 1\n ELSE current_list_schema_version END\n WHERE role = $1\n RETURNING\n role, title, description, form_schema, metadata_schema, properties,\n ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,\n priority_threshold_minutes, priority_facet,\n current_schema_version, list_schema, current_list_schema_version,\n default_pins\n ), snapshot AS (\n INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)\n SELECT role, current_schema_version, form_schema, metadata_schema, $26\n FROM updated\n WHERE ($6::boolean OR $8::boolean) AND current_schema_version IS NOT NULL\n ON CONFLICT (role, version) DO NOTHING\n ), list_snapshot AS (\n INSERT INTO lt_role_list_schemas (role, version, list_schema, change_summary)\n SELECT role, current_list_schema_version, list_schema, $26\n FROM updated\n WHERE $29::boolean AND current_list_schema_version IS NOT NULL\n ON CONFLICT (role, version) DO NOTHING\n ), upstream_prune AS (\n DELETE FROM lt_role_upstreams\n WHERE $27::boolean AND role = $1 AND upstream_role <> ALL($28::text[])\n ), upstream_add AS (\n INSERT INTO lt_role_upstreams (role, upstream_role)\n SELECT $1, u FROM unnest($28::text[]) AS u\n WHERE $27::boolean\n ON CONFLICT DO NOTHING\n )\n SELECT * FROM updated";
|
|
50
|
+
export declare const UPDATE_ROLE_METADATA = "\n WITH updated AS (\n UPDATE lt_roles SET\n title = CASE WHEN $2::boolean THEN $3 ELSE title END,\n description = CASE WHEN $4::boolean THEN $5 ELSE description END,\n form_schema = CASE WHEN $6::boolean THEN $7::jsonb ELSE form_schema END,\n metadata_schema = CASE WHEN $8::boolean THEN $9::jsonb ELSE metadata_schema END,\n properties = CASE WHEN $10::boolean THEN COALESCE($11::jsonb, '{}'::jsonb) ELSE properties END,\n ops_visible = CASE WHEN $12::boolean THEN $13::boolean ELSE ops_visible END,\n parent_role = CASE WHEN $14::boolean THEN $15 ELSE parent_role END,\n sla_minutes = CASE WHEN $16::boolean THEN $17::numeric ELSE sla_minutes END,\n target_per_hour = CASE WHEN $18::boolean THEN $19::numeric ELSE target_per_hour END,\n worker_count = CASE WHEN $20::boolean THEN $21::int ELSE worker_count END,\n priority_threshold_minutes\n = CASE WHEN $22::boolean THEN $23::numeric ELSE priority_threshold_minutes END,\n priority_facet = CASE WHEN $24::boolean THEN $25 ELSE priority_facet END,\n list_schema = CASE WHEN $29::boolean THEN $30::jsonb ELSE list_schema END,\n default_pins = CASE WHEN $31::boolean THEN $32::jsonb ELSE default_pins END,\n enforce_schema = CASE WHEN $33::boolean THEN COALESCE($34::boolean, false) ELSE enforce_schema END,\n current_schema_version = CASE\n WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)\n OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)\n THEN COALESCE(current_schema_version, 0) + 1\n ELSE current_schema_version END,\n current_list_schema_version = CASE\n WHEN $29::boolean AND $30::jsonb IS DISTINCT FROM list_schema\n THEN COALESCE(current_list_schema_version, 0) + 1\n ELSE current_list_schema_version END\n WHERE role = $1\n RETURNING\n role, title, description, form_schema, metadata_schema, properties,\n ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,\n priority_threshold_minutes, priority_facet,\n current_schema_version, list_schema, current_list_schema_version,\n default_pins, enforce_schema\n ), snapshot AS (\n INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)\n SELECT role, current_schema_version, form_schema, metadata_schema, $26\n FROM updated\n WHERE ($6::boolean OR $8::boolean) AND current_schema_version IS NOT NULL\n ON CONFLICT (role, version) DO NOTHING\n ), list_snapshot AS (\n INSERT INTO lt_role_list_schemas (role, version, list_schema, change_summary)\n SELECT role, current_list_schema_version, list_schema, $26\n FROM updated\n WHERE $29::boolean AND current_list_schema_version IS NOT NULL\n ON CONFLICT (role, version) DO NOTHING\n ), upstream_prune AS (\n DELETE FROM lt_role_upstreams\n WHERE $27::boolean AND role = $1 AND upstream_role <> ALL($28::text[])\n ), upstream_add AS (\n INSERT INTO lt_role_upstreams (role, upstream_role)\n SELECT $1, u FROM unnest($28::text[]) AS u\n WHERE $27::boolean\n ON CONFLICT DO NOTHING\n )\n SELECT * FROM updated";
|
|
47
51
|
export declare const GET_ROLE_FORM_SCHEMA = "\n SELECT form_schema FROM lt_roles WHERE role = $1";
|
|
52
|
+
/**
|
|
53
|
+
* The set of roles with server-side resolver validation turned on. Small and
|
|
54
|
+
* slow-changing — the enforcement cache holds it under a short TTL so the
|
|
55
|
+
* resolve hot path costs zero SQL when no role (or no involved role) enforces.
|
|
56
|
+
*/
|
|
57
|
+
export declare const LIST_ENFORCING_ROLES = "\n SELECT role FROM lt_roles WHERE enforce_schema";
|
|
48
58
|
export declare const GET_ROLE_METADATA_SCHEMA = "\n SELECT metadata_schema FROM lt_roles WHERE role = $1";
|
|
49
59
|
export declare const GET_ROLE_UPSTREAMS = "\n SELECT upstream_role FROM lt_role_upstreams WHERE role = $1 ORDER BY upstream_role";
|
|
50
60
|
export declare const LIST_ROLE_SCHEMA_VERSIONS = "\n SELECT\n s.version,\n s.form_schema IS NOT NULL AS has_form_schema,\n s.metadata_schema IS NOT NULL AS has_metadata_schema,\n s.change_summary,\n s.created_at,\n s.version = r.current_schema_version AS is_current\n FROM lt_role_schemas s\n JOIN lt_roles r ON r.role = s.role\n WHERE s.role = $1\n ORDER BY s.version DESC";
|
|
@@ -61,7 +71,7 @@ export declare const GET_ROLE_LIST_SCHEMA_VERSION = "\n SELECT s.role, s.versio
|
|
|
61
71
|
* never versioned its list schema still answers, with version NULL).
|
|
62
72
|
*/
|
|
63
73
|
export declare const GET_ROLE_LIST_SCHEMA_CURRENT = "\n SELECT r.role, r.current_list_schema_version AS version, r.list_schema,\n s.change_summary, s.created_at,\n r.current_list_schema_version AS latest_version\n FROM lt_roles r\n LEFT JOIN lt_role_list_schemas s\n ON s.role = r.role AND s.version = r.current_list_schema_version\n WHERE r.role = $1";
|
|
64
|
-
export declare const LIST_ROLES_WITH_DETAILS = "\n WITH\n user_counts AS (\n SELECT role, COUNT(DISTINCT user_id)::int AS cnt\n FROM lt_user_roles\n GROUP BY role\n ),\n chain_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM (\n SELECT source_role AS role FROM lt_config_role_escalations\n UNION ALL\n SELECT target_role AS role FROM lt_config_role_escalations\n ) c\n GROUP BY role\n ),\n workflow_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM lt_config_roles\n GROUP BY role\n ),\n upstreams AS (\n SELECT role, array_agg(upstream_role ORDER BY upstream_role) AS ups\n FROM lt_role_upstreams\n GROUP BY role\n )\n SELECT\n r.role,\n r.title,\n r.description,\n r.form_schema,\n r.metadata_schema,\n r.properties,\n r.ops_visible,\n r.parent_role,\n r.sla_minutes,\n r.target_per_hour,\n r.worker_count,\n r.priority_threshold_minutes,\n r.priority_facet,\n r.current_schema_version,\n r.list_schema,\n r.current_list_schema_version,\n r.default_pins,\n COALESCE(up.ups, '{}') AS upstream_roles,\n COALESCE(uc.cnt, 0) AS user_count,\n COALESCE(cc.cnt, 0) AS chain_count,\n COALESCE(wc.cnt, 0) AS workflow_count\n FROM lt_roles r\n LEFT JOIN user_counts uc ON uc.role = r.role\n LEFT JOIN chain_counts cc ON cc.role = r.role\n LEFT JOIN workflow_counts wc ON wc.role = r.role\n LEFT JOIN upstreams up ON up.role = r.role\n ORDER BY r.role";
|
|
74
|
+
export declare const LIST_ROLES_WITH_DETAILS = "\n WITH\n user_counts AS (\n SELECT role, COUNT(DISTINCT user_id)::int AS cnt\n FROM lt_user_roles\n GROUP BY role\n ),\n chain_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM (\n SELECT source_role AS role FROM lt_config_role_escalations\n UNION ALL\n SELECT target_role AS role FROM lt_config_role_escalations\n ) c\n GROUP BY role\n ),\n workflow_counts AS (\n SELECT role, COUNT(*)::int AS cnt\n FROM lt_config_roles\n GROUP BY role\n ),\n upstreams AS (\n SELECT role, array_agg(upstream_role ORDER BY upstream_role) AS ups\n FROM lt_role_upstreams\n GROUP BY role\n )\n SELECT\n r.role,\n r.title,\n r.description,\n r.form_schema,\n r.metadata_schema,\n r.properties,\n r.ops_visible,\n r.parent_role,\n r.sla_minutes,\n r.target_per_hour,\n r.worker_count,\n r.priority_threshold_minutes,\n r.priority_facet,\n r.current_schema_version,\n r.list_schema,\n r.current_list_schema_version,\n r.default_pins,\n r.enforce_schema,\n COALESCE(up.ups, '{}') AS upstream_roles,\n COALESCE(uc.cnt, 0) AS user_count,\n COALESCE(cc.cnt, 0) AS chain_count,\n COALESCE(wc.cnt, 0) AS workflow_count\n FROM lt_roles r\n LEFT JOIN user_counts uc ON uc.role = r.role\n LEFT JOIN chain_counts cc ON cc.role = r.role\n LEFT JOIN workflow_counts wc ON wc.role = r.role\n LEFT JOIN upstreams up ON up.role = r.role\n ORDER BY r.role";
|
|
65
75
|
export declare const COUNT_USER_ROLE_REFS = "\n SELECT COUNT(*)::int AS cnt FROM lt_user_roles WHERE role = $1";
|
|
66
76
|
export declare const COUNT_CHAIN_REFS = "\n SELECT COUNT(*)::int AS cnt\n FROM lt_config_role_escalations\n WHERE source_role = $1 OR target_role = $1";
|
|
67
77
|
export declare const COUNT_WORKFLOW_REFS = "\n SELECT COUNT(*)::int AS cnt FROM lt_config_roles WHERE role = $1";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// ─── Escalation chain queries ───────────────────────────────────────────────
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.COUNT_ACTIVE_ESCALATION_REFS = exports.COUNT_WORKFLOW_REFS = exports.COUNT_CHAIN_REFS = exports.COUNT_USER_ROLE_REFS = exports.LIST_ROLES_WITH_DETAILS = exports.GET_ROLE_LIST_SCHEMA_CURRENT = exports.GET_ROLE_LIST_SCHEMA_VERSION = exports.LIST_ROLE_LIST_SCHEMA_VERSIONS = exports.GET_ROLE_SCHEMA_CURRENT = exports.GET_ROLE_SCHEMA_VERSION = exports.LIST_ROLE_SCHEMA_VERSIONS = exports.GET_ROLE_UPSTREAMS = exports.GET_ROLE_METADATA_SCHEMA = exports.GET_ROLE_FORM_SCHEMA = exports.UPDATE_ROLE_METADATA = exports.DELETE_ROLE = exports.LIST_ROLES = exports.ENSURE_ROLE_EXISTS = exports.CHECK_ESCALATION_CHAIN_EXISTS = exports.DELETE_ESCALATION_CHAINS_BY_SOURCE = exports.DELETE_ESCALATION_CHAIN = exports.ADD_ESCALATION_CHAIN = exports.INSERT_ESCALATION_CHAIN = exports.GET_ALL_ESCALATION_CHAINS = exports.GET_ESCALATION_TARGETS = void 0;
|
|
4
|
+
exports.COUNT_ACTIVE_ESCALATION_REFS = exports.COUNT_WORKFLOW_REFS = exports.COUNT_CHAIN_REFS = exports.COUNT_USER_ROLE_REFS = exports.LIST_ROLES_WITH_DETAILS = exports.GET_ROLE_LIST_SCHEMA_CURRENT = exports.GET_ROLE_LIST_SCHEMA_VERSION = exports.LIST_ROLE_LIST_SCHEMA_VERSIONS = exports.GET_ROLE_SCHEMA_CURRENT = exports.GET_ROLE_SCHEMA_VERSION = exports.LIST_ROLE_SCHEMA_VERSIONS = exports.GET_ROLE_UPSTREAMS = exports.GET_ROLE_METADATA_SCHEMA = exports.LIST_ENFORCING_ROLES = exports.GET_ROLE_FORM_SCHEMA = exports.UPDATE_ROLE_METADATA = exports.DELETE_ROLE = exports.LIST_ROLES = exports.ENSURE_ROLE_EXISTS = exports.CHECK_ESCALATION_CHAIN_EXISTS = exports.DELETE_ESCALATION_CHAINS_BY_SOURCE = exports.DELETE_ESCALATION_CHAIN = exports.ADD_ESCALATION_CHAIN = exports.INSERT_ESCALATION_CHAIN = exports.GET_ALL_ESCALATION_CHAINS = exports.GET_ESCALATION_TARGETS = void 0;
|
|
5
5
|
exports.GET_ESCALATION_TARGETS = `
|
|
6
6
|
SELECT target_role
|
|
7
7
|
FROM lt_config_role_escalations
|
|
@@ -76,6 +76,10 @@ exports.DELETE_ROLE = `
|
|
|
76
76
|
*
|
|
77
77
|
* default_pins ($31 = provided sentinel, $32 = value) is unversioned config:
|
|
78
78
|
* the pinned-view seeds a role hands its members.
|
|
79
|
+
*
|
|
80
|
+
* enforce_schema ($33 = provided sentinel, $34 = value) is unversioned config:
|
|
81
|
+
* the server-side resolver-validation opt-in. The service layer invalidates
|
|
82
|
+
* the enforcement cache after this statement commits.
|
|
79
83
|
*/
|
|
80
84
|
exports.UPDATE_ROLE_METADATA = `
|
|
81
85
|
WITH updated AS (
|
|
@@ -95,6 +99,7 @@ exports.UPDATE_ROLE_METADATA = `
|
|
|
95
99
|
priority_facet = CASE WHEN $24::boolean THEN $25 ELSE priority_facet END,
|
|
96
100
|
list_schema = CASE WHEN $29::boolean THEN $30::jsonb ELSE list_schema END,
|
|
97
101
|
default_pins = CASE WHEN $31::boolean THEN $32::jsonb ELSE default_pins END,
|
|
102
|
+
enforce_schema = CASE WHEN $33::boolean THEN COALESCE($34::boolean, false) ELSE enforce_schema END,
|
|
98
103
|
current_schema_version = CASE
|
|
99
104
|
WHEN ($6::boolean AND $7::jsonb IS DISTINCT FROM form_schema)
|
|
100
105
|
OR ($8::boolean AND $9::jsonb IS DISTINCT FROM metadata_schema)
|
|
@@ -110,7 +115,7 @@ exports.UPDATE_ROLE_METADATA = `
|
|
|
110
115
|
ops_visible, parent_role, sla_minutes, target_per_hour, worker_count,
|
|
111
116
|
priority_threshold_minutes, priority_facet,
|
|
112
117
|
current_schema_version, list_schema, current_list_schema_version,
|
|
113
|
-
default_pins
|
|
118
|
+
default_pins, enforce_schema
|
|
114
119
|
), snapshot AS (
|
|
115
120
|
INSERT INTO lt_role_schemas (role, version, form_schema, metadata_schema, change_summary)
|
|
116
121
|
SELECT role, current_schema_version, form_schema, metadata_schema, $26
|
|
@@ -135,6 +140,13 @@ exports.UPDATE_ROLE_METADATA = `
|
|
|
135
140
|
SELECT * FROM updated`;
|
|
136
141
|
exports.GET_ROLE_FORM_SCHEMA = `
|
|
137
142
|
SELECT form_schema FROM lt_roles WHERE role = $1`;
|
|
143
|
+
/**
|
|
144
|
+
* The set of roles with server-side resolver validation turned on. Small and
|
|
145
|
+
* slow-changing — the enforcement cache holds it under a short TTL so the
|
|
146
|
+
* resolve hot path costs zero SQL when no role (or no involved role) enforces.
|
|
147
|
+
*/
|
|
148
|
+
exports.LIST_ENFORCING_ROLES = `
|
|
149
|
+
SELECT role FROM lt_roles WHERE enforce_schema`;
|
|
138
150
|
exports.GET_ROLE_METADATA_SCHEMA = `
|
|
139
151
|
SELECT metadata_schema FROM lt_roles WHERE role = $1`;
|
|
140
152
|
exports.GET_ROLE_UPSTREAMS = `
|
|
@@ -245,6 +257,7 @@ exports.LIST_ROLES_WITH_DETAILS = `
|
|
|
245
257
|
r.list_schema,
|
|
246
258
|
r.current_list_schema_version,
|
|
247
259
|
r.default_pins,
|
|
260
|
+
r.enforce_schema,
|
|
248
261
|
COALESCE(up.ups, '{}') AS upstream_roles,
|
|
249
262
|
COALESCE(uc.cnt, 0) AS user_count,
|
|
250
263
|
COALESCE(cc.cnt, 0) AS chain_count,
|
|
@@ -54,6 +54,13 @@ export interface RoleDetail {
|
|
|
54
54
|
list_schema: Record<string, any> | null;
|
|
55
55
|
/** Version of the live list_schema; null until the role first carries one. */
|
|
56
56
|
current_list_schema_version: number | null;
|
|
57
|
+
/**
|
|
58
|
+
* When true, every resolve surface validates the submitted resolverPayload
|
|
59
|
+
* against the escalation's resolved form schema (same pass the dashboard
|
|
60
|
+
* runs) and rejects violations with 422 before any state changes. Opt-in
|
|
61
|
+
* per role; false leaves resolution behavior unchanged.
|
|
62
|
+
*/
|
|
63
|
+
enforce_schema: boolean;
|
|
57
64
|
/**
|
|
58
65
|
* Pinned-view seeds this role hands its members: [{ label, url, badge? }].
|
|
59
66
|
* On first render, a member without an own pin of the same label sees these
|
|
@@ -133,6 +140,8 @@ export interface UpdateRoleInput {
|
|
|
133
140
|
worker_count?: number | null;
|
|
134
141
|
priority_threshold_minutes?: number | null;
|
|
135
142
|
priority_facet?: string | null;
|
|
143
|
+
/** Turn server-side resolver schema validation on/off for this role. */
|
|
144
|
+
enforce_schema?: boolean;
|
|
136
145
|
/**
|
|
137
146
|
* Replace the upstream-input set (omitted = preserve; null or [] = clear).
|
|
138
147
|
* Every entry must name an existing role other than this one.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Human-readable label for a schema field.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. The JSON Schema `title` keyword — the author's explicit label, always wins.
|
|
6
|
+
* 2. Snake/kebab keys become Title Case: `left_quantity` → `Left Quantity`.
|
|
7
|
+
* 3. Single-token keys pass through unchanged: `PO`, `SKU`, `LEFTQUANTITY` —
|
|
8
|
+
* a run of capitals carries no word boundaries to recover, and lowercasing
|
|
9
|
+
* would mangle acronyms. Authors who want prose labels declare `title`.
|
|
10
|
+
*
|
|
11
|
+
* Used everywhere a field name faces a human: form labels, dictionary lists,
|
|
12
|
+
* the errors panel, and metadata displays.
|
|
13
|
+
*/
|
|
14
|
+
export declare function deriveFieldLabel(key: string, fieldSchema?: Record<string, unknown> | null): string;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.deriveFieldLabel = deriveFieldLabel;
|
|
4
|
+
/**
|
|
5
|
+
* Human-readable label for a schema field.
|
|
6
|
+
*
|
|
7
|
+
* Resolution order:
|
|
8
|
+
* 1. The JSON Schema `title` keyword — the author's explicit label, always wins.
|
|
9
|
+
* 2. Snake/kebab keys become Title Case: `left_quantity` → `Left Quantity`.
|
|
10
|
+
* 3. Single-token keys pass through unchanged: `PO`, `SKU`, `LEFTQUANTITY` —
|
|
11
|
+
* a run of capitals carries no word boundaries to recover, and lowercasing
|
|
12
|
+
* would mangle acronyms. Authors who want prose labels declare `title`.
|
|
13
|
+
*
|
|
14
|
+
* Used everywhere a field name faces a human: form labels, dictionary lists,
|
|
15
|
+
* the errors panel, and metadata displays.
|
|
16
|
+
*/
|
|
17
|
+
function deriveFieldLabel(key, fieldSchema) {
|
|
18
|
+
const title = fieldSchema?.title;
|
|
19
|
+
if (typeof title === 'string' && title.trim().length > 0)
|
|
20
|
+
return title.trim();
|
|
21
|
+
const words = key.split(/[_-]+/).filter(Boolean);
|
|
22
|
+
if (words.length <= 1)
|
|
23
|
+
return key;
|
|
24
|
+
return words
|
|
25
|
+
.map((w) => (isCapsToken(w) ? w : w.charAt(0).toUpperCase() + w.slice(1)))
|
|
26
|
+
.join(' ');
|
|
27
|
+
}
|
|
28
|
+
/** All-caps tokens (PO, SKU, ID) keep their casing inside multi-word keys. */
|
|
29
|
+
function isCapsToken(word) {
|
|
30
|
+
return word.length > 1 && word === word.toUpperCase() && /[A-Z]/.test(word);
|
|
31
|
+
}
|