@hotmeshio/long-tail 0.10.13 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -4
- package/build/api/escalations/analytics.d.ts +6 -0
- package/build/api/escalations/analytics.js +123 -0
- package/build/api/escalations/index.d.ts +1 -0
- package/build/api/escalations/index.js +4 -1
- package/build/api/escalations/metadata.js +1 -1
- package/build/api/escalations/resolve.js +4 -4
- package/build/api/roles.d.ts +4 -0
- package/build/api/roles.js +8 -0
- package/build/api/scan-codes/choice.d.ts +11 -0
- package/build/api/scan-codes/choice.js +136 -0
- package/build/api/scan-codes/context.d.ts +24 -0
- package/build/api/scan-codes/context.js +86 -0
- package/build/api/scan-codes/execute.d.ts +1 -0
- package/build/api/scan-codes/execute.js +49 -244
- package/build/api/scan-codes/identity.d.ts +18 -0
- package/build/api/scan-codes/identity.js +116 -0
- package/build/api/scan-codes/index.d.ts +2 -0
- package/build/api/scan-codes/index.js +6 -1
- package/build/api/scan-codes/locate.d.ts +14 -0
- package/build/api/scan-codes/locate.js +127 -0
- package/build/api/scan-codes/present.d.ts +9 -0
- package/build/api/scan-codes/present.js +59 -0
- package/build/api/scan-codes/verbs.d.ts +15 -0
- package/build/api/scan-codes/verbs.js +205 -0
- package/build/bin/ltc.js +33 -0
- package/build/lib/cli/commands/escalations.d.ts +31 -0
- package/build/lib/cli/commands/escalations.js +88 -0
- package/build/lib/db/schemas/028_role_entity_facet.sql +18 -0
- package/build/lib/db/schemas/029_scan_scheme_kind.sql +31 -0
- package/build/modules/config.d.ts +5 -0
- package/build/modules/config.js +12 -0
- package/build/routes/escalations/acting.d.ts +12 -0
- package/build/routes/escalations/acting.js +25 -0
- package/build/routes/escalations/analytics.d.ts +8 -0
- package/build/routes/escalations/analytics.js +63 -0
- package/build/routes/escalations/index.js +3 -0
- package/build/routes/escalations/resolve.js +8 -1
- package/build/routes/escalations/single.js +13 -4
- package/build/routes/roles.js +1 -1
- package/build/routes/scan-codes.js +28 -2
- package/build/scripts/ortho-populate.js +14 -0
- package/build/scripts/printer-cycle.d.ts +25 -0
- package/build/scripts/printer-cycle.js +169 -0
- package/build/scripts/twin-farm.js +19 -5
- package/build/sdk/index.d.ts +2 -0
- package/build/sdk/index.js +3 -0
- package/build/services/escalation/aggregate-sql.d.ts +36 -0
- package/build/services/escalation/aggregate-sql.js +253 -0
- package/build/services/escalation/aggregate-validate.d.ts +57 -0
- package/build/services/escalation/aggregate-validate.js +218 -0
- package/build/services/escalation/aggregates.d.ts +26 -0
- package/build/services/escalation/aggregates.js +204 -0
- package/build/services/escalation/client.js +57 -21
- package/build/services/escalation/facet-sql.js +17 -0
- package/build/services/escalation/index.d.ts +2 -0
- package/build/services/escalation/index.js +8 -1
- package/build/services/escalation/metrics-cache.d.ts +18 -0
- package/build/services/escalation/metrics-cache.js +47 -1
- package/build/services/escalation/queries.js +2 -0
- package/build/services/escalation/resolver-validation.d.ts +1 -1
- package/build/services/escalation/resolver-validation.js +12 -5
- package/build/services/escalation/submit-guard.d.ts +29 -0
- package/build/services/escalation/submit-guard.js +57 -0
- package/build/services/iam/acting-identity.d.ts +16 -0
- package/build/services/iam/acting-identity.js +60 -0
- package/build/services/role/index.js +2 -0
- package/build/services/role/sql.d.ts +10 -2
- package/build/services/role/sql.js +14 -1
- package/build/services/role/types.d.ts +16 -0
- package/build/services/scan-code/read.js +4 -0
- package/build/services/scan-code/sql.d.ts +4 -4
- package/build/services/scan-code/sql.js +14 -8
- package/build/services/scan-code/validate.d.ts +2 -0
- package/build/services/scan-code/validate.js +86 -0
- package/build/services/scan-code/write.d.ts +4 -0
- package/build/services/scan-code/write.js +16 -2
- package/build/services/user/crud.d.ts +6 -0
- package/build/services/user/crud.js +16 -0
- package/build/services/user/index.d.ts +1 -1
- package/build/services/user/index.js +2 -1
- package/build/services/user/sql.d.ts +8 -0
- package/build/services/user/sql.js +9 -1
- package/build/shared/form-validation/index.d.ts +4 -0
- package/build/shared/form-validation/index.js +4 -0
- package/build/shared/form-validation/x-lt-labels.d.ts +28 -0
- package/build/shared/form-validation/x-lt-labels.js +42 -0
- package/build/shared/form-validation/x-lt-query.d.ts +56 -0
- package/build/shared/form-validation/x-lt-query.js +49 -0
- package/build/shared/form-validation/x-lt-submit-guard.d.ts +46 -0
- package/build/shared/form-validation/x-lt-submit-guard.js +25 -0
- package/build/shared/form-validation/x-lt-submit-on-claim.d.ts +22 -0
- package/build/shared/form-validation/x-lt-submit-on-claim.js +28 -0
- package/build/system/mcp-servers/admin/escalations.js +34 -0
- package/build/system/mcp-servers/admin/scan-codes.js +17 -1
- package/build/system/mcp-servers/admin/schemas.d.ts +643 -25
- package/build/system/mcp-servers/admin/schemas.js +127 -19
- package/build/system/mcp-servers/admin/users.js +3 -1
- package/build/system/mcp-servers/human-queue-schemas.d.ts +2 -2
- package/build/system/seed/tool-manifests-admin.d.ts +2283 -0
- package/build/system/seed/tool-manifests-admin.js +8 -5
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/analytics.d.ts +154 -0
- package/build/types/analytics.js +14 -0
- package/build/types/facets.d.ts +13 -0
- package/build/types/index.d.ts +5 -2
- package/build/types/index.js +5 -1
- package/build/types/scan-choice.d.ts +49 -0
- package/build/types/scan-choice.js +9 -0
- package/build/types/scan-code.d.ts +72 -1
- package/build/types/scan-code.js +30 -1
- package/dashboard/dist/assets/{AdminDashboard-Bv3pHBzn.js → AdminDashboard-DJ3LJQ09.js} +2 -2
- package/dashboard/dist/assets/{AdminDashboard-Bv3pHBzn.js.map → AdminDashboard-DJ3LJQ09.js.map} +1 -1
- package/dashboard/dist/assets/{AgentConfigPage-Dk2zlvDb.js → AgentConfigPage-BMMvaD0z.js} +2 -2
- package/dashboard/dist/assets/{AgentConfigPage-Dk2zlvDb.js.map → AgentConfigPage-BMMvaD0z.js.map} +1 -1
- package/dashboard/dist/assets/{AgentDetailPage-DUno6EGs.js → AgentDetailPage-C6wfsnRe.js} +2 -2
- package/dashboard/dist/assets/{AgentDetailPage-DUno6EGs.js.map → AgentDetailPage-C6wfsnRe.js.map} +1 -1
- package/dashboard/dist/assets/AgentsPage-Cwqka7nE.js +2 -0
- package/dashboard/dist/assets/{AgentsPage-CToVu1qj.js.map → AgentsPage-Cwqka7nE.js.map} +1 -1
- package/dashboard/dist/assets/AvailableEscalationsPage-CVlG_AVC.js +2 -0
- package/dashboard/dist/assets/AvailableEscalationsPage-CVlG_AVC.js.map +1 -0
- package/dashboard/dist/assets/{BotPicker-DQOpVB4M.js → BotPicker-BBkJBXZB.js} +2 -2
- package/dashboard/dist/assets/{BotPicker-DQOpVB4M.js.map → BotPicker-BBkJBXZB.js.map} +1 -1
- package/dashboard/dist/assets/{CapabilitiesPage-Dmf46PUJ.js → CapabilitiesPage-wxmE4917.js} +2 -2
- package/dashboard/dist/assets/{CapabilitiesPage-Dmf46PUJ.js.map → CapabilitiesPage-wxmE4917.js.map} +1 -1
- package/dashboard/dist/assets/{CollapsibleSection-CDADefUY.js → CollapsibleSection-B3FsrHwH.js} +2 -2
- package/dashboard/dist/assets/{CollapsibleSection-CDADefUY.js.map → CollapsibleSection-B3FsrHwH.js.map} +1 -1
- package/dashboard/dist/assets/CountdownTimer-B3nCqJdC.js +2 -0
- package/dashboard/dist/assets/CountdownTimer-B3nCqJdC.js.map +1 -0
- package/dashboard/dist/assets/{CredentialsPage-8LIEvomi.js → CredentialsPage-Bv875IHe.js} +2 -2
- package/dashboard/dist/assets/{CredentialsPage-8LIEvomi.js.map → CredentialsPage-Bv875IHe.js.map} +1 -1
- package/dashboard/dist/assets/{CronLabel-CK3WSsM5.js → CronLabel-CSzjWuwj.js} +2 -2
- package/dashboard/dist/assets/{CronLabel-CK3WSsM5.js.map → CronLabel-CSzjWuwj.js.map} +1 -1
- package/dashboard/dist/assets/{CustomDurationPicker-FPmlA6cE.js → CustomDurationPicker-CCpqf3e4.js} +2 -2
- package/dashboard/dist/assets/{CustomDurationPicker-FPmlA6cE.js.map → CustomDurationPicker-CCpqf3e4.js.map} +1 -1
- package/dashboard/dist/assets/{DropZone-CL9gA7pu.js → DropZone-CJkk-U3o.js} +2 -2
- package/dashboard/dist/assets/{DropZone-CL9gA7pu.js.map → DropZone-CJkk-U3o.js.map} +1 -1
- package/dashboard/dist/assets/{ElapsedCell-BZVkVxE0.js → ElapsedCell-WV6pUaKo.js} +2 -2
- package/dashboard/dist/assets/{ElapsedCell-BZVkVxE0.js.map → ElapsedCell-WV6pUaKo.js.map} +1 -1
- package/dashboard/dist/assets/EntityTimelinePanel-DrL86Yrf.js +2 -0
- package/dashboard/dist/assets/EntityTimelinePanel-DrL86Yrf.js.map +1 -0
- package/dashboard/dist/assets/{EscalationListSchemaPage-kYLdUtZX.js → EscalationListSchemaPage-CTQCALw4.js} +3 -3
- package/dashboard/dist/assets/{EscalationListSchemaPage-kYLdUtZX.js.map → EscalationListSchemaPage-CTQCALw4.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationSchemaPage-kUmCDivo.js → EscalationSchemaPage-D_UgP0pv.js} +3 -3
- package/dashboard/dist/assets/{EscalationSchemaPage-kUmCDivo.js.map → EscalationSchemaPage-D_UgP0pv.js.map} +1 -1
- package/dashboard/dist/assets/{EscalationsOverview-D17auO-m.js → EscalationsOverview-DuIUyxr0.js} +2 -2
- package/dashboard/dist/assets/{EscalationsOverview-D17auO-m.js.map → EscalationsOverview-DuIUyxr0.js.map} +1 -1
- package/dashboard/dist/assets/EventTable-BV6unphX.js +2 -0
- package/dashboard/dist/assets/EventTable-BV6unphX.js.map +1 -0
- package/dashboard/dist/assets/FilterBar-DYFgzXNB.js +2 -0
- package/dashboard/dist/assets/FilterBar-DYFgzXNB.js.map +1 -0
- package/dashboard/dist/assets/{GraphInvokePage-D11uOnZi.js → GraphInvokePage-BGHVUipJ.js} +2 -2
- package/dashboard/dist/assets/{GraphInvokePage-D11uOnZi.js.map → GraphInvokePage-BGHVUipJ.js.map} +1 -1
- package/dashboard/dist/assets/HomePage-B-S2cK3E.js +2 -0
- package/dashboard/dist/assets/{HomePage-Bcq10Bxx.js.map → HomePage-B-S2cK3E.js.map} +1 -1
- package/dashboard/dist/assets/ListToolbar-Bl7F3A28.js +2 -0
- package/dashboard/dist/assets/{ListToolbar-IPXsTb3Q.js.map → ListToolbar-Bl7F3A28.js.map} +1 -1
- package/dashboard/dist/assets/{McpOverview-Dx2j0KkF.js → McpOverview-_T9IsCD_.js} +2 -2
- package/dashboard/dist/assets/{McpOverview-Dx2j0KkF.js.map → McpOverview-_T9IsCD_.js.map} +1 -1
- package/dashboard/dist/assets/{McpQueryDetailPage-CzF_bna8.js → McpQueryDetailPage-DtsVqKPw.js} +2 -2
- package/dashboard/dist/assets/{McpQueryDetailPage-CzF_bna8.js.map → McpQueryDetailPage-DtsVqKPw.js.map} +1 -1
- package/dashboard/dist/assets/McpQueryPage-DX5u-1bx.js +2 -0
- package/dashboard/dist/assets/{McpQueryPage-CNsuiw_q.js.map → McpQueryPage-DX5u-1bx.js.map} +1 -1
- package/dashboard/dist/assets/McpRunDetailPage-BhcqqtYS.js +2 -0
- package/dashboard/dist/assets/McpRunDetailPage-BhcqqtYS.js.map +1 -0
- package/dashboard/dist/assets/{McpRunsPage-CK96s3Nt.js → McpRunsPage-L-1y8wnX.js} +2 -2
- package/dashboard/dist/assets/{McpRunsPage-CK96s3Nt.js.map → McpRunsPage-L-1y8wnX.js.map} +1 -1
- package/dashboard/dist/assets/{NamespacePill-B9Vc_Pz2.js → NamespacePill-in1SLd0W.js} +2 -2
- package/dashboard/dist/assets/{NamespacePill-B9Vc_Pz2.js.map → NamespacePill-in1SLd0W.js.map} +1 -1
- package/dashboard/dist/assets/OperationsPage-i8KcDpKo.js +3 -0
- package/dashboard/dist/assets/OperationsPage-i8KcDpKo.js.map +1 -0
- package/dashboard/dist/assets/OperatorDashboard-CIbDzOlh.js +2 -0
- package/dashboard/dist/assets/{OperatorDashboard-C-jE0IRA.js.map → OperatorDashboard-CIbDzOlh.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeader-Ce4GcIYo.js → PageHeader-BJgWMZtw.js} +2 -2
- package/dashboard/dist/assets/{PageHeader-Ce4GcIYo.js.map → PageHeader-BJgWMZtw.js.map} +1 -1
- package/dashboard/dist/assets/{PageHeaderWithStats-Ck0KZvXD.js → PageHeaderWithStats-Ccio4vyG.js} +2 -2
- package/dashboard/dist/assets/{PageHeaderWithStats-Ck0KZvXD.js.map → PageHeaderWithStats-Ccio4vyG.js.map} +1 -1
- package/dashboard/dist/assets/{PersonaDetailPage-CJ6jqo7h.js → PersonaDetailPage-BOaf0TW1.js} +2 -2
- package/dashboard/dist/assets/{PersonaDetailPage-CJ6jqo7h.js.map → PersonaDetailPage-BOaf0TW1.js.map} +1 -1
- package/dashboard/dist/assets/{PersonasPage-D8hL3kL5.js → PersonasPage-DSdRb90f.js} +2 -2
- package/dashboard/dist/assets/{PersonasPage-D8hL3kL5.js.map → PersonasPage-DSdRb90f.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessDetailPage-Vnx8Z5_y.js → ProcessDetailPage-DN9NqfeP.js} +2 -2
- package/dashboard/dist/assets/{ProcessDetailPage-Vnx8Z5_y.js.map → ProcessDetailPage-DN9NqfeP.js.map} +1 -1
- package/dashboard/dist/assets/{ProcessesListPage-DRYtvS3N.js → ProcessesListPage-CfQEWaD4.js} +2 -2
- package/dashboard/dist/assets/{ProcessesListPage-DRYtvS3N.js.map → ProcessesListPage-CfQEWaD4.js.map} +1 -1
- package/dashboard/dist/assets/RoleDetailPage-BC5HL7ls.js +8 -0
- package/dashboard/dist/assets/RoleDetailPage-BC5HL7ls.js.map +1 -0
- package/dashboard/dist/assets/{RolePill-CnuBPhPw.js → RolePill-BNAP-uK6.js} +2 -2
- package/dashboard/dist/assets/{RolePill-CnuBPhPw.js.map → RolePill-BNAP-uK6.js.map} +1 -1
- package/dashboard/dist/assets/RolesPage-DCnW0VmD.js +2 -0
- package/dashboard/dist/assets/RolesPage-DCnW0VmD.js.map +1 -0
- package/dashboard/dist/assets/RunAsSelector-twbuX7fB.js +2 -0
- package/dashboard/dist/assets/{RunAsSelector-CS3KQ-BF.js.map → RunAsSelector-twbuX7fB.js.map} +1 -1
- package/dashboard/dist/assets/ScanCodesPage-CPNg6K--.js +2 -0
- package/dashboard/dist/assets/ScanCodesPage-CPNg6K--.js.map +1 -0
- package/dashboard/dist/assets/ScanSchemeDetailPage-DyLmCamO.js +2 -0
- package/dashboard/dist/assets/ScanSchemeDetailPage-DyLmCamO.js.map +1 -0
- package/dashboard/dist/assets/ScanStationPage-DoZccU5B.js +2 -0
- package/dashboard/dist/assets/ScanStationPage-DoZccU5B.js.map +1 -0
- package/dashboard/dist/assets/{ScopeBadge-BUpYpS3x.js → ScopeBadge-BG7KkZ8Q.js} +2 -2
- package/dashboard/dist/assets/{ScopeBadge-BUpYpS3x.js.map → ScopeBadge-BG7KkZ8Q.js.map} +1 -1
- package/dashboard/dist/assets/SegmentedTabs-DXNzY05q.js +2 -0
- package/dashboard/dist/assets/SegmentedTabs-DXNzY05q.js.map +1 -0
- package/dashboard/dist/assets/StickyPagination-DIeYMPPd.js +2 -0
- package/dashboard/dist/assets/StickyPagination-DIeYMPPd.js.map +1 -0
- package/dashboard/dist/assets/{StreamMessageDetail-DyJyZwBl.js → StreamMessageDetail-gU4CUeQI.js} +2 -2
- package/dashboard/dist/assets/{StreamMessageDetail-DyJyZwBl.js.map → StreamMessageDetail-gU4CUeQI.js.map} +1 -1
- package/dashboard/dist/assets/{SwimlaneTimeline-BlzV74V5.js → SwimlaneTimeline-Br-nBA4l.js} +2 -2
- package/dashboard/dist/assets/{SwimlaneTimeline-BlzV74V5.js.map → SwimlaneTimeline-Br-nBA4l.js.map} +1 -1
- package/dashboard/dist/assets/{TagInput-BbZ25nNe.js → TagInput-CvM5n6ww.js} +2 -2
- package/dashboard/dist/assets/{TagInput-BbZ25nNe.js.map → TagInput-CvM5n6ww.js.map} +1 -1
- package/dashboard/dist/assets/{TaskDetailPage-B0p_Z4J_.js → TaskDetailPage-_9cr_R0u.js} +2 -2
- package/dashboard/dist/assets/{TaskDetailPage-B0p_Z4J_.js.map → TaskDetailPage-_9cr_R0u.js.map} +1 -1
- package/dashboard/dist/assets/{TaskQueuePill-D0QKBrN3.js → TaskQueuePill-D4JPHXOq.js} +2 -2
- package/dashboard/dist/assets/{TaskQueuePill-D0QKBrN3.js.map → TaskQueuePill-D4JPHXOq.js.map} +1 -1
- package/dashboard/dist/assets/{TasksListPage-CiGR2QiQ.js → TasksListPage-DVENC9Jp.js} +2 -2
- package/dashboard/dist/assets/{TasksListPage-CiGR2QiQ.js.map → TasksListPage-DVENC9Jp.js.map} +1 -1
- package/dashboard/dist/assets/{TimeAgo-D_DA9ucC.js → TimeAgo-C9yYnOui.js} +2 -2
- package/dashboard/dist/assets/{TimeAgo-D_DA9ucC.js.map → TimeAgo-C9yYnOui.js.map} +1 -1
- package/dashboard/dist/assets/{TimestampCell-C7rkC9nh.js → TimestampCell-DoB8cYgw.js} +2 -2
- package/dashboard/dist/assets/{TimestampCell-C7rkC9nh.js.map → TimestampCell-DoB8cYgw.js.map} +1 -1
- package/dashboard/dist/assets/{ToolPill-8xY9rkoc.js → ToolPill-BUjbC-Tj.js} +2 -2
- package/dashboard/dist/assets/{ToolPill-8xY9rkoc.js.map → ToolPill-BUjbC-Tj.js.map} +1 -1
- package/dashboard/dist/assets/{ToolTestPanel-E8efYGYs.js → ToolTestPanel-CBtsIP8K.js} +2 -2
- package/dashboard/dist/assets/{ToolTestPanel-E8efYGYs.js.map → ToolTestPanel-CBtsIP8K.js.map} +1 -1
- package/dashboard/dist/assets/{TopicDetailPage-B455XrRc.js → TopicDetailPage-mR4Jaz83.js} +4 -4
- package/dashboard/dist/assets/{TopicDetailPage-B455XrRc.js.map → TopicDetailPage-mR4Jaz83.js.map} +1 -1
- package/dashboard/dist/assets/TopicsPage-CP7HFxIC.js +2 -0
- package/dashboard/dist/assets/{TopicsPage-1VAc29Qw.js.map → TopicsPage-CP7HFxIC.js.map} +1 -1
- package/dashboard/dist/assets/{UserName-DikzjQHJ.js → UserName-BnLF6yhz.js} +2 -2
- package/dashboard/dist/assets/{UserName-DikzjQHJ.js.map → UserName-BnLF6yhz.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowExecutionPage-xL956RZi.js +2 -0
- package/dashboard/dist/assets/WorkflowExecutionPage-xL956RZi.js.map +1 -0
- package/dashboard/dist/assets/{WorkflowPill-Da406nEj.js → WorkflowPill-tXU0-eXu.js} +2 -2
- package/dashboard/dist/assets/{WorkflowPill-Da406nEj.js.map → WorkflowPill-tXU0-eXu.js.map} +1 -1
- package/dashboard/dist/assets/WorkflowsDashboard-B-u7zFKM.js +2 -0
- package/dashboard/dist/assets/{WorkflowsDashboard-BMQd_0tW.js.map → WorkflowsDashboard-B-u7zFKM.js.map} +1 -1
- package/dashboard/dist/assets/{WorkflowsOverview-CdxjVifp.js → WorkflowsOverview-CqZVarK-.js} +2 -2
- package/dashboard/dist/assets/{WorkflowsOverview-CdxjVifp.js.map → WorkflowsOverview-CqZVarK-.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DNVffIXa.js → YamlWorkflowDetailPage-DiFLavW0.js} +10 -10
- package/dashboard/dist/assets/{YamlWorkflowDetailPage-DNVffIXa.js.map → YamlWorkflowDetailPage-DiFLavW0.js.map} +1 -1
- package/dashboard/dist/assets/{YamlWorkflowsPage-BGP8wuos.js → YamlWorkflowsPage-BuQM6uZA.js} +2 -2
- package/dashboard/dist/assets/{YamlWorkflowsPage-BGP8wuos.js.map → YamlWorkflowsPage-BuQM6uZA.js.map} +1 -1
- package/dashboard/dist/assets/{agents-BZJLIJ4h.js → agents-DU9GPovp.js} +2 -2
- package/dashboard/dist/assets/{agents-BZJLIJ4h.js.map → agents-DU9GPovp.js.map} +1 -1
- package/dashboard/dist/assets/{bots-ClLcDGJb.js → bots-Cy-Hmg7w.js} +2 -2
- package/dashboard/dist/assets/{bots-ClLcDGJb.js.map → bots-Cy-Hmg7w.js.map} +1 -1
- package/dashboard/dist/assets/capabilities-sSJJqrp-.js +2 -0
- package/dashboard/dist/assets/{capabilities-IaCcS7ge.js.map → capabilities-sSJJqrp-.js.map} +1 -1
- package/dashboard/dist/assets/{controlplane-Zi4m_MSx.js → controlplane-TVCjDHQ-.js} +2 -2
- package/dashboard/dist/assets/{controlplane-Zi4m_MSx.js.map → controlplane-TVCjDHQ-.js.map} +1 -1
- package/dashboard/dist/assets/escalation-columns-BpkjKYGd.js +2 -0
- package/dashboard/dist/assets/escalation-columns-BpkjKYGd.js.map +1 -0
- package/dashboard/dist/assets/index-5TblaCXQ.css +1 -0
- package/dashboard/dist/assets/index-7BP6FaCZ.js +2 -0
- package/dashboard/dist/assets/{index-Dldi3Q3M.js.map → index-7BP6FaCZ.js.map} +1 -1
- package/dashboard/dist/assets/{index-BzkE48xJ.js → index-B7IaBPBg.js} +4 -4
- package/dashboard/dist/assets/{index-BzkE48xJ.js.map → index-B7IaBPBg.js.map} +1 -1
- package/dashboard/dist/assets/index-BE0phFw0.js +2 -0
- package/dashboard/dist/assets/{index-CLmZaNTK.js.map → index-BE0phFw0.js.map} +1 -1
- package/dashboard/dist/assets/{index-BqGnhkh0.js → index-BYNN9N1T.js} +2 -2
- package/dashboard/dist/assets/{index-BqGnhkh0.js.map → index-BYNN9N1T.js.map} +1 -1
- package/dashboard/dist/assets/{index-kiKZresb.js → index-CLPVqbVY.js} +2 -2
- package/dashboard/dist/assets/{index-kiKZresb.js.map → index-CLPVqbVY.js.map} +1 -1
- package/dashboard/dist/assets/{index-C3o4OisO.js → index-Cfv86_L9.js} +2 -2
- package/dashboard/dist/assets/{index-C3o4OisO.js.map → index-Cfv86_L9.js.map} +1 -1
- package/dashboard/dist/assets/{index-yGNt29V9.js → index-CtwbQ_-m.js} +2 -2
- package/dashboard/dist/assets/{index-yGNt29V9.js.map → index-CtwbQ_-m.js.map} +1 -1
- package/dashboard/dist/assets/index-DOai950R.js +3 -0
- package/dashboard/dist/assets/index-DOai950R.js.map +1 -0
- package/dashboard/dist/assets/index-DTtHNC-D.js +5 -0
- package/dashboard/dist/assets/{index-CvNBdkB1.js.map → index-DTtHNC-D.js.map} +1 -1
- package/dashboard/dist/assets/{index-CJu2PBNx.js → index-DsWnVyVN.js} +2 -2
- package/dashboard/dist/assets/{index-CJu2PBNx.js.map → index-DsWnVyVN.js.map} +1 -1
- package/dashboard/dist/assets/{index-D0zFReGL.js → index-Dy-MMYti.js} +2 -2
- package/dashboard/dist/assets/{index-D0zFReGL.js.map → index-Dy-MMYti.js.map} +1 -1
- package/dashboard/dist/assets/index-errgQT5x.js +65 -0
- package/dashboard/dist/assets/index-errgQT5x.js.map +1 -0
- package/dashboard/dist/assets/{index-CmnNqpWg.js → index-fgBDhnI6.js} +2 -2
- package/dashboard/dist/assets/{index-CmnNqpWg.js.map → index-fgBDhnI6.js.map} +1 -1
- package/dashboard/dist/assets/{knowledge-DWLxGTib.js → knowledge-arh4NfmL.js} +2 -2
- package/dashboard/dist/assets/{knowledge-DWLxGTib.js.map → knowledge-arh4NfmL.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-B6AARZ2I.js → mcp-QGf5jHCo.js} +2 -2
- package/dashboard/dist/assets/{mcp-B6AARZ2I.js.map → mcp-QGf5jHCo.js.map} +1 -1
- package/dashboard/dist/assets/{mcp-query-DrkvBU3a.js → mcp-query-BEJN1nMZ.js} +2 -2
- package/dashboard/dist/assets/{mcp-query-DrkvBU3a.js.map → mcp-query-BEJN1nMZ.js.map} +1 -1
- package/dashboard/dist/assets/{personas-Cmz2SAEU.js → personas-DYtw1COf.js} +2 -2
- package/dashboard/dist/assets/{personas-Cmz2SAEU.js.map → personas-DYtw1COf.js.map} +1 -1
- package/dashboard/dist/assets/{pipelines-CSgfVCMy.js → pipelines-Byi6PnbE.js} +2 -2
- package/dashboard/dist/assets/{pipelines-CSgfVCMy.js.map → pipelines-Byi6PnbE.js.map} +1 -1
- package/dashboard/dist/assets/{tasks-UOtUkDk-.js → tasks-D0SQdENz.js} +2 -2
- package/dashboard/dist/assets/{tasks-UOtUkDk-.js.map → tasks-D0SQdENz.js.map} +1 -1
- package/dashboard/dist/assets/{topics-Df5hGh6o.js → topics-DgDv5ZXP.js} +2 -2
- package/dashboard/dist/assets/{topics-Df5hGh6o.js.map → topics-DgDv5ZXP.js.map} +1 -1
- package/dashboard/dist/assets/useEventHooks-DZaYzE7s.js +2 -0
- package/dashboard/dist/assets/{useEventHooks-PDbuSJ12.js.map → useEventHooks-DZaYzE7s.js.map} +1 -1
- package/dashboard/dist/assets/{useNamespace-tZSOlge3.js → useNamespace-C1OqiNXN.js} +2 -2
- package/dashboard/dist/assets/{useNamespace-tZSOlge3.js.map → useNamespace-C1OqiNXN.js.map} +1 -1
- package/dashboard/dist/assets/{useYamlActivityEvents-CINLzE32.js → useYamlActivityEvents-CkmXDm-Z.js} +2 -2
- package/dashboard/dist/assets/{useYamlActivityEvents-CINLzE32.js.map → useYamlActivityEvents-CkmXDm-Z.js.map} +1 -1
- package/dashboard/dist/assets/{users-G8qwyZKS.js → users-D4DCFPH1.js} +2 -2
- package/dashboard/dist/assets/{users-G8qwyZKS.js.map → users-D4DCFPH1.js.map} +1 -1
- package/dashboard/dist/assets/{vendor-icons-BpZiWH2f.js → vendor-icons-BxuyFxH2.js} +156 -136
- package/dashboard/dist/assets/vendor-icons-BxuyFxH2.js.map +1 -0
- package/dashboard/dist/assets/{workflows-DbPqi1TW.js → workflows-qyc0ku_5.js} +2 -2
- package/dashboard/dist/assets/{workflows-DbPqi1TW.js.map → workflows-qyc0ku_5.js.map} +1 -1
- package/dashboard/dist/assets/{yaml-workflows-DozD8hpR.js → yaml-workflows-DWFww9Xn.js} +2 -2
- package/dashboard/dist/assets/{yaml-workflows-DozD8hpR.js.map → yaml-workflows-DWFww9Xn.js.map} +1 -1
- package/dashboard/dist/index.html +3 -3
- package/docs/api/http/escalations.md +133 -0
- package/docs/api/http/roles.md +6 -0
- package/docs/api/mcp/admin.md +53 -0
- package/docs/api/sdk/escalations.md +58 -0
- package/docs/cli.md +11 -0
- package/docs/dashboard.md +22 -20
- package/docs/data.md +12 -4
- package/docs/design-principles.md +9 -7
- package/docs/escalation-analytics.md +251 -0
- package/docs/hitl/roles.md +1 -1
- package/docs/hitl/x-lt-embed.md +8 -4
- package/docs/hitl/x-lt-footer.md +67 -0
- package/docs/hitl/x-lt-list-schema.md +10 -3
- package/docs/hitl-guide.md +11 -2
- package/docs/iam.md +12 -0
- package/docs/operations.md +17 -0
- package/docs/scan-codes.md +112 -0
- package/docs/sdk.md +1 -1
- package/package.json +2 -1
- package/dashboard/dist/assets/AgentsPage-CToVu1qj.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-DldboL4W.js +0 -2
- package/dashboard/dist/assets/AvailableEscalationsPage-DldboL4W.js.map +0 -1
- package/dashboard/dist/assets/CountdownTimer-DPjIkerB.js +0 -2
- package/dashboard/dist/assets/CountdownTimer-DPjIkerB.js.map +0 -1
- package/dashboard/dist/assets/EventTable-CwPWl163.js +0 -2
- package/dashboard/dist/assets/EventTable-CwPWl163.js.map +0 -1
- package/dashboard/dist/assets/FilterBar-D7GghUsU.js +0 -2
- package/dashboard/dist/assets/FilterBar-D7GghUsU.js.map +0 -1
- package/dashboard/dist/assets/HomePage-Bcq10Bxx.js +0 -2
- package/dashboard/dist/assets/ListToolbar-IPXsTb3Q.js +0 -2
- package/dashboard/dist/assets/McpQueryPage-CNsuiw_q.js +0 -2
- package/dashboard/dist/assets/McpRunDetailPage-BJguEMS0.js +0 -2
- package/dashboard/dist/assets/McpRunDetailPage-BJguEMS0.js.map +0 -1
- package/dashboard/dist/assets/OperationsPage-DvWrLuH6.js +0 -2
- package/dashboard/dist/assets/OperationsPage-DvWrLuH6.js.map +0 -1
- package/dashboard/dist/assets/OperatorDashboard-C-jE0IRA.js +0 -2
- package/dashboard/dist/assets/RoleDetailPage-BKkUvTO-.js +0 -8
- package/dashboard/dist/assets/RoleDetailPage-BKkUvTO-.js.map +0 -1
- package/dashboard/dist/assets/RolesPage-BHzWK9n6.js +0 -2
- package/dashboard/dist/assets/RolesPage-BHzWK9n6.js.map +0 -1
- package/dashboard/dist/assets/RunAsSelector-CS3KQ-BF.js +0 -2
- package/dashboard/dist/assets/ScanCodesPage-BW4P4zWf.js +0 -2
- package/dashboard/dist/assets/ScanCodesPage-BW4P4zWf.js.map +0 -1
- package/dashboard/dist/assets/ScanSchemeDetailPage-BLykA4j-.js +0 -2
- package/dashboard/dist/assets/ScanSchemeDetailPage-BLykA4j-.js.map +0 -1
- package/dashboard/dist/assets/StickyPagination-hMrnLXde.js +0 -2
- package/dashboard/dist/assets/StickyPagination-hMrnLXde.js.map +0 -1
- package/dashboard/dist/assets/TopicsPage-1VAc29Qw.js +0 -2
- package/dashboard/dist/assets/WorkflowExecutionPage-C15GSAoe.js +0 -2
- package/dashboard/dist/assets/WorkflowExecutionPage-C15GSAoe.js.map +0 -1
- package/dashboard/dist/assets/WorkflowsDashboard-BMQd_0tW.js +0 -2
- package/dashboard/dist/assets/capabilities-IaCcS7ge.js +0 -2
- package/dashboard/dist/assets/escalation-columns-BY2mg9DY.js +0 -2
- package/dashboard/dist/assets/escalation-columns-BY2mg9DY.js.map +0 -1
- package/dashboard/dist/assets/index-CLmZaNTK.js +0 -2
- package/dashboard/dist/assets/index-CfAi1OsI.js +0 -65
- package/dashboard/dist/assets/index-CfAi1OsI.js.map +0 -1
- package/dashboard/dist/assets/index-CvNBdkB1.js +0 -5
- package/dashboard/dist/assets/index-Dldi3Q3M.js +0 -2
- package/dashboard/dist/assets/index-mKcQGi2B.js +0 -3
- package/dashboard/dist/assets/index-mKcQGi2B.js.map +0 -1
- package/dashboard/dist/assets/index-zQiUPfxA.css +0 -1
- package/dashboard/dist/assets/useEventHooks-PDbuSJ12.js +0 -2
- package/dashboard/dist/assets/vendor-icons-BpZiWH2f.js.map +0 -1
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// Escalation analytics service — grouped/temporal reads over the interval
|
|
4
|
+
// time-series (see aggregate-sql.ts for the SQL semantics). Pure read surface:
|
|
5
|
+
// no lifecycle change, no write path.
|
|
6
|
+
//
|
|
7
|
+
// Caching splits by anchor. A now-anchored result (membership now, a window
|
|
8
|
+
// reaching now) is pace-board-grade: short TTL, cleared by
|
|
9
|
+
// invalidateEscalationAggregates() on every escalation write on this
|
|
10
|
+
// container. A fully-past anchor is immutable under every long-tail write
|
|
11
|
+
// path — created_at is set at insert and terminal transitions stamp NOW(),
|
|
12
|
+
// which is strictly after any past anchor — so those entries survive
|
|
13
|
+
// invalidation and live on the longer TTL (bounding the documented SDK
|
|
14
|
+
// post-terminal-touch caveat rather than caching forever). Keys canonicalize
|
|
15
|
+
// the whole input, so both caches are LRU-bounded.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.clearNowAnchoredAnalyticsCache = clearNowAnchoredAnalyticsCache;
|
|
19
|
+
exports.resetAnalyticsCaches = resetAnalyticsCaches;
|
|
20
|
+
exports.resolveEntitySystem = resolveEntitySystem;
|
|
21
|
+
exports.aggregateByFacets = aggregateByFacets;
|
|
22
|
+
exports.timelineByFacet = timelineByFacet;
|
|
23
|
+
const db_1 = require("../../lib/db");
|
|
24
|
+
const config_1 = require("../../modules/config");
|
|
25
|
+
const facets_1 = require("./facets");
|
|
26
|
+
const metrics_cache_1 = require("./metrics-cache");
|
|
27
|
+
const aggregate_sql_1 = require("./aggregate-sql");
|
|
28
|
+
const aggregate_validate_1 = require("./aggregate-validate");
|
|
29
|
+
const nowAnchoredCache = new metrics_cache_1.BoundedTtlCache(config_1.config.LT_ANALYTICS_CACHE_TTL_MS, config_1.config.LT_ANALYTICS_CACHE_MAX_ENTRIES);
|
|
30
|
+
const pastAnchoredCache = new metrics_cache_1.BoundedTtlCache(config_1.config.LT_ANALYTICS_PAST_CACHE_TTL_MS, config_1.config.LT_ANALYTICS_CACHE_MAX_ENTRIES);
|
|
31
|
+
/** Key-order-independent stringify, so `{a,b}` and `{b,a}` share one entry. */
|
|
32
|
+
function canonical(value) {
|
|
33
|
+
return JSON.stringify(value, (_key, v) => v && typeof v === 'object' && !Array.isArray(v)
|
|
34
|
+
? Object.fromEntries(Object.entries(v).sort(([a], [b]) => (a < b ? -1 : 1)))
|
|
35
|
+
: v);
|
|
36
|
+
}
|
|
37
|
+
/** Called on every escalation write (via invalidateEscalationAggregates). */
|
|
38
|
+
function clearNowAnchoredAnalyticsCache() {
|
|
39
|
+
nowAnchoredCache.clear();
|
|
40
|
+
}
|
|
41
|
+
/** Test hook: drop BOTH analytics caches. */
|
|
42
|
+
function resetAnalyticsCaches() {
|
|
43
|
+
nowAnchoredCache.clear();
|
|
44
|
+
pastAnchoredCache.clear();
|
|
45
|
+
}
|
|
46
|
+
function matchesState(row, m) {
|
|
47
|
+
if (m.role !== undefined && row.role !== m.role)
|
|
48
|
+
return false;
|
|
49
|
+
if (m.roles !== undefined && !m.roles.includes(row.role))
|
|
50
|
+
return false;
|
|
51
|
+
if (m.subtype !== undefined && row.subtype !== m.subtype)
|
|
52
|
+
return false;
|
|
53
|
+
for (const [key, value] of Object.entries(m.facets ?? {})) {
|
|
54
|
+
if (row.facets[key] == null || row.facets[key] !== String(value))
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
for (const key of m.exists ?? []) {
|
|
58
|
+
if (row.facets[key] == null)
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function toAggregateRow(raw, built, states) {
|
|
64
|
+
const facets = {};
|
|
65
|
+
for (const key of built.facets)
|
|
66
|
+
facets[key] = raw[`facet_${key}`] ?? null;
|
|
67
|
+
const row = { facets, sampleCount: Number(raw.sample_count) };
|
|
68
|
+
for (const column of built.columns)
|
|
69
|
+
row[column] = raw[column] ?? undefined;
|
|
70
|
+
if (built.measureKind === 'membership')
|
|
71
|
+
row.count = Number(raw.count);
|
|
72
|
+
else
|
|
73
|
+
row.dwellSeconds = Number(raw.dwell_seconds);
|
|
74
|
+
if (built.stateGrouped) {
|
|
75
|
+
row.state = raw.state_label;
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
const label = (states ?? []).find((s) => matchesState(row, s.match));
|
|
79
|
+
if (label)
|
|
80
|
+
row.state = label.name;
|
|
81
|
+
}
|
|
82
|
+
return row;
|
|
83
|
+
}
|
|
84
|
+
function toIso(value) {
|
|
85
|
+
return value instanceof Date ? value.toISOString() : new Date(value).toISOString();
|
|
86
|
+
}
|
|
87
|
+
function toTimelineInterval(raw, built) {
|
|
88
|
+
const facets = {};
|
|
89
|
+
for (const key of built.facets)
|
|
90
|
+
facets[key] = raw[`facet_${key}`] ?? null;
|
|
91
|
+
const interval = {
|
|
92
|
+
role: raw.role,
|
|
93
|
+
facets,
|
|
94
|
+
startedAt: toIso(raw.started_at),
|
|
95
|
+
endedAt: raw.ended_at == null ? null : toIso(raw.ended_at),
|
|
96
|
+
durationSeconds: Number(raw.duration_seconds),
|
|
97
|
+
};
|
|
98
|
+
if (built.columns.includes('subtype'))
|
|
99
|
+
interval.subtype = raw.subtype ?? undefined;
|
|
100
|
+
if (built.columns.includes('status'))
|
|
101
|
+
interval.status = raw.status;
|
|
102
|
+
return interval;
|
|
103
|
+
}
|
|
104
|
+
// ── Entity-system resolution ─────────────────────────────────────────────────
|
|
105
|
+
const ENTITY_SYSTEM_SQL = `
|
|
106
|
+
SELECT role, entity_state_source FROM lt_roles WHERE entity_facet = $1 ORDER BY role`;
|
|
107
|
+
const STATE_SOURCES_SQL = `
|
|
108
|
+
SELECT role, entity_state_source FROM lt_roles WHERE role = ANY($1::text[])`;
|
|
109
|
+
/**
|
|
110
|
+
* The roles declaring `entity_facet = key` — the entity's SYSTEM — with each
|
|
111
|
+
* role's state naming. Loud when nothing declares the key: an empty system is
|
|
112
|
+
* a config gap, not an empty result.
|
|
113
|
+
*/
|
|
114
|
+
async function resolveEntitySystem(entityKey) {
|
|
115
|
+
(0, aggregate_validate_1.requireFacetKey)(entityKey, 'query.entity');
|
|
116
|
+
await (0, facets_1.ensureFacetReady)();
|
|
117
|
+
const { rows } = await (0, db_1.getPool)().query(ENTITY_SYSTEM_SQL, [entityKey]);
|
|
118
|
+
if (rows.length === 0) {
|
|
119
|
+
throw new aggregate_sql_1.AnalyticsInputError(`no roles declare entity_facet "${entityKey}" — set Entity on the roles this entity moves through`);
|
|
120
|
+
}
|
|
121
|
+
return rows.map((r) => ({
|
|
122
|
+
role: r.role,
|
|
123
|
+
source: r.entity_state_source === 'subtype' ? 'subtype' : 'role',
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
/** State naming for an explicit role list (roles missing from lt_roles default to 'role'). */
|
|
127
|
+
async function stateSourcesFor(roles) {
|
|
128
|
+
const { rows } = await (0, db_1.getPool)().query(STATE_SOURCES_SQL, [roles]);
|
|
129
|
+
const sources = {};
|
|
130
|
+
for (const role of roles)
|
|
131
|
+
sources[role] = 'role';
|
|
132
|
+
for (const r of rows)
|
|
133
|
+
sources[r.role] = r.entity_state_source === 'subtype' ? 'subtype' : 'role';
|
|
134
|
+
return sources;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Resolve `query.entity` (and `groupBy.state`) into concrete scope: the
|
|
138
|
+
* system's roles replace `entity` on the filter (plus `exists: [entityKey]`
|
|
139
|
+
* so rows without the entity stay out), and per-role state sources ride along
|
|
140
|
+
* for the builder. The resolved system joins the cache key, so a role-config
|
|
141
|
+
* edit changes the key rather than serving a stale state shape.
|
|
142
|
+
*/
|
|
143
|
+
async function resolveScope(query, needsStateSources) {
|
|
144
|
+
if (query.entity !== undefined) {
|
|
145
|
+
const system = await resolveEntitySystem(query.entity);
|
|
146
|
+
const { entity, ...rest } = query;
|
|
147
|
+
const resolved = {
|
|
148
|
+
...rest,
|
|
149
|
+
roles: system.map((s) => s.role),
|
|
150
|
+
exists: [...new Set([...(rest.exists ?? []), entity])],
|
|
151
|
+
};
|
|
152
|
+
const sources = {};
|
|
153
|
+
for (const s of system)
|
|
154
|
+
sources[s.role] = s.source;
|
|
155
|
+
return { query: resolved, stateSources: sources, cacheSuffix: `:${canonical(system)}` };
|
|
156
|
+
}
|
|
157
|
+
if (needsStateSources) {
|
|
158
|
+
const roles = query.role ? [query.role] : query.roles ?? [];
|
|
159
|
+
if (roles.length === 0)
|
|
160
|
+
return { query, cacheSuffix: '' }; // builder rejects loudly
|
|
161
|
+
const sources = await stateSourcesFor(roles);
|
|
162
|
+
return { query, stateSources: sources, cacheSuffix: `:${canonical(sources)}` };
|
|
163
|
+
}
|
|
164
|
+
return { query, cacheSuffix: '' };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Grouped aggregate over the escalation intervals — membership at an instant
|
|
168
|
+
* or dwell over a window, grouped by columns + facets (+ the derived state
|
|
169
|
+
* label). One call replaces N countByFacets round-trips.
|
|
170
|
+
*/
|
|
171
|
+
async function aggregateByFacets(input) {
|
|
172
|
+
const scope = await resolveScope(input.query ?? {}, input.groupBy?.state === true);
|
|
173
|
+
const effective = { ...input, query: scope.query };
|
|
174
|
+
// Validates loudly BEFORE any cache entry.
|
|
175
|
+
const built = (0, aggregate_sql_1.buildAggregateQuery)(effective, { stateSources: scope.stateSources });
|
|
176
|
+
const cache = built.nowAnchored ? nowAnchoredCache : pastAnchoredCache;
|
|
177
|
+
return (await cache.resolve(`agg:${canonical(input)}${scope.cacheSuffix}`, async () => {
|
|
178
|
+
await (0, facets_1.ensureFacetReady)();
|
|
179
|
+
const { rows } = await (0, db_1.getPool)().query(built.sql, built.params);
|
|
180
|
+
return {
|
|
181
|
+
groups: rows.slice(0, built.pageLimit).map((r) => toAggregateRow(r, built, input.states)),
|
|
182
|
+
overflow: rows.length > built.pageLimit,
|
|
183
|
+
};
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* One entity's ordered interval sequence — `[created_at, ended_at)` spans with
|
|
188
|
+
* durations. Gaps between consecutive intervals are untracked time and are
|
|
189
|
+
* deliberately preserved, not filled.
|
|
190
|
+
*/
|
|
191
|
+
async function timelineByFacet(input) {
|
|
192
|
+
const scope = await resolveScope(input.query ?? {}, false);
|
|
193
|
+
const effective = { ...input, query: scope.query };
|
|
194
|
+
const built = (0, aggregate_sql_1.buildTimelineQuery)(effective);
|
|
195
|
+
const cache = built.nowAnchored ? nowAnchoredCache : pastAnchoredCache;
|
|
196
|
+
return (await cache.resolve(`tl:${canonical(input)}${scope.cacheSuffix}`, async () => {
|
|
197
|
+
await (0, facets_1.ensureFacetReady)();
|
|
198
|
+
const { rows } = await (0, db_1.getPool)().query(built.sql, built.params);
|
|
199
|
+
return {
|
|
200
|
+
intervals: rows.slice(0, built.pageLimit).map((r) => toTimelineInterval(r, built)),
|
|
201
|
+
overflow: rows.length > built.pageLimit,
|
|
202
|
+
};
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
@@ -97,16 +97,24 @@ async function installEscalationCompatView() {
|
|
|
97
97
|
await client.query('SELECT pg_advisory_unlock($1)', [COMPAT_VIEW_LOCK_ID]).catch(() => { });
|
|
98
98
|
client.release();
|
|
99
99
|
}
|
|
100
|
-
// The
|
|
100
|
+
// The indexes long-tail owns build OUTSIDE the advisory lock:
|
|
101
101
|
// CREATE INDEX CONCURRENTLY waits on every transaction holding a snapshot,
|
|
102
102
|
// and sibling containers parked inside `SELECT pg_advisory_lock(...)` hold
|
|
103
103
|
// exactly that — building inside the critical section would stall the build
|
|
104
104
|
// against its own waiters. Out here the build blocks nothing (writes proceed
|
|
105
105
|
// during CONCURRENTLY) and nothing blocks it.
|
|
106
|
-
await
|
|
106
|
+
await ensureAppIndex('idx_hmsh_esc_resolved_cover', ENSURE_RESOLVED_COVER_INDEX);
|
|
107
|
+
await ensureAppIndex('idx_hmsh_esc_ended_at', ENSURE_ENDED_AT_INDEX);
|
|
107
108
|
}
|
|
109
|
+
/** Reports existence + validity so a crash-orphaned INVALID build is rebuilt. */
|
|
110
|
+
const APP_INDEX_STATE = `
|
|
111
|
+
SELECT i.indisvalid AS valid
|
|
112
|
+
FROM pg_class c
|
|
113
|
+
JOIN pg_index i ON i.indexrelid = c.oid
|
|
114
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
115
|
+
WHERE c.relname = $1 AND n.nspname = 'public'`;
|
|
108
116
|
/**
|
|
109
|
-
* Idempotent, concurrent-boot-safe build of
|
|
117
|
+
* Idempotent, concurrent-boot-safe build of one app-layer index.
|
|
110
118
|
*
|
|
111
119
|
* A crashed CONCURRENTLY build leaves an INVALID index behind, and
|
|
112
120
|
* `CREATE INDEX CONCURRENTLY IF NOT EXISTS` would then skip it forever — so
|
|
@@ -114,20 +122,20 @@ async function installEscalationCompatView() {
|
|
|
114
122
|
* A create that loses a cross-container race logs and defers to the next
|
|
115
123
|
* boot, which observes the winner's valid index and no-ops.
|
|
116
124
|
*/
|
|
117
|
-
async function
|
|
125
|
+
async function ensureAppIndex(name, ddl) {
|
|
118
126
|
const pool = (0, db_1.getPool)();
|
|
119
127
|
try {
|
|
120
|
-
const { rows } = await pool.query(
|
|
128
|
+
const { rows } = await pool.query(APP_INDEX_STATE, [name]);
|
|
121
129
|
if (rows[0]?.valid === true)
|
|
122
130
|
return;
|
|
123
131
|
if (rows.length > 0) {
|
|
124
|
-
await pool.query(
|
|
132
|
+
await pool.query(`DROP INDEX IF EXISTS ${name}`);
|
|
125
133
|
}
|
|
126
|
-
await pool.query(
|
|
127
|
-
logger_1.loggerRegistry.info(
|
|
134
|
+
await pool.query(ddl);
|
|
135
|
+
logger_1.loggerRegistry.info(`[escalation] ${name} ensured`);
|
|
128
136
|
}
|
|
129
137
|
catch (err) {
|
|
130
|
-
logger_1.loggerRegistry.warn(`[escalation]
|
|
138
|
+
logger_1.loggerRegistry.warn(`[escalation] ${name} index build deferred to next boot: ${err.message}`);
|
|
131
139
|
}
|
|
132
140
|
}
|
|
133
141
|
// Migrate legacy `lt_escalations` rows into `hmsh_escalations` (idempotent),
|
|
@@ -188,16 +196,38 @@ BEGIN
|
|
|
188
196
|
END IF;
|
|
189
197
|
END $$;`;
|
|
190
198
|
// `available` mirrors the legacy isEffectivelyClaimed/isAvailable heuristic so
|
|
191
|
-
// existing `SELECT *` consumers are unaffected; the
|
|
199
|
+
// existing `SELECT *` consumers are unaffected; the columns are additive.
|
|
200
|
+
//
|
|
201
|
+
// `ended_at` is the instant the row left the live set: resolve stamps
|
|
202
|
+
// resolved_at in its guarded transition; cancel and expire stamp only
|
|
203
|
+
// updated_at in theirs, and no long-tail write path touches a terminal row
|
|
204
|
+
// afterward, so COALESCE(resolved_at, updated_at) is the transition instant
|
|
205
|
+
// for all three terminal statuses. (The SDK's updateEscalation /
|
|
206
|
+
// appendEscalationMilestones carry no status guard, so a post-terminal touch
|
|
207
|
+
// WOULD skew a cancelled/expired end — documented contract, not enforced.)
|
|
208
|
+
// Analytics SQL inlines the equivalent expression rather than reading this
|
|
209
|
+
// column (so non-default liveStatuses compose and the partial index predicate
|
|
210
|
+
// stays provable); the view column serves external and debugging consumers.
|
|
211
|
+
// If the SDK ever ships a physical ended_at column, the SELECT * expansion
|
|
212
|
+
// collides with this alias — remediation is dropping the view-level column
|
|
213
|
+
// and reading the (strictly better) real one.
|
|
214
|
+
//
|
|
215
|
+
// DROP+CREATE (not CREATE OR REPLACE) because the base table's column set
|
|
216
|
+
// moves with SDK migrations: REPLACE re-expands SELECT * and errors the
|
|
217
|
+
// moment a new base column lands before the computed aliases. The swap runs
|
|
218
|
+
// inside the install transaction, so readers see the old view or the new
|
|
219
|
+
// one, never a gap.
|
|
192
220
|
const CREATE_COMPAT_VIEW = `
|
|
193
|
-
|
|
221
|
+
DROP VIEW IF EXISTS public.lt_escalations;
|
|
222
|
+
CREATE VIEW public.lt_escalations AS
|
|
194
223
|
SELECT *,
|
|
195
|
-
(assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()) AS available
|
|
224
|
+
(assigned_to IS NULL OR assigned_until IS NULL OR assigned_until <= NOW()) AS available,
|
|
225
|
+
(CASE WHEN status <> 'pending' THEN COALESCE(resolved_at, updated_at) END) AS ended_at
|
|
196
226
|
FROM public.hmsh_escalations;`;
|
|
197
227
|
// Index ownership — hmsh 0.25.0 ships the general aggregate indexes on
|
|
198
228
|
// hmsh_escalations (idx_hmsh_esc_stats_pending covers the pending backlog
|
|
199
229
|
// incl. assigned_to/assigned_until; _stats_created and _stats_resolved bound
|
|
200
|
-
// the created/resolved windows). Long-tail keeps exactly
|
|
230
|
+
// the created/resolved windows). Long-tail keeps exactly TWO app-layer indexes:
|
|
201
231
|
//
|
|
202
232
|
// idx_hmsh_esc_resolved_cover (role, resolved_at DESC, claimed_at, created_at)
|
|
203
233
|
// WHERE status = 'resolved'
|
|
@@ -206,6 +236,15 @@ CREATE OR REPLACE VIEW public.lt_escalations AS
|
|
|
206
236
|
// trailing claimed_at/created_at feed PERCENTILE_CONT(resolved_at -
|
|
207
237
|
// claimed_at) and (claimed_at - created_at) from the index.
|
|
208
238
|
//
|
|
239
|
+
// idx_hmsh_esc_ended_at ((COALESCE(resolved_at, updated_at)))
|
|
240
|
+
// WHERE status <> 'pending'
|
|
241
|
+
// Serves the analytics interval scans (aggregate-sql.ts): dwell and
|
|
242
|
+
// past-membership need "terminal rows whose end instant is after the
|
|
243
|
+
// window/asOf" — without this, every such query scans all terminal
|
|
244
|
+
// history, growing without bound. Write cost is the cheapest possible:
|
|
245
|
+
// inserts are 'pending' (excluded by the predicate), so exactly one
|
|
246
|
+
// index write occurs per row, at its terminal transition.
|
|
247
|
+
//
|
|
209
248
|
// Everything else this branch once created is dropped below: the pending
|
|
210
249
|
// index duplicated _stats_pending, and the claimed cover had no consuming
|
|
211
250
|
// query — both were pure write amplification on the hot claim/resolve path.
|
|
@@ -218,15 +257,12 @@ const DROP_SUPERSEDED_PENDING_INDEX = `
|
|
|
218
257
|
DROP INDEX IF EXISTS idx_hmsh_esc_pending_role_created`;
|
|
219
258
|
const DROP_UNCONSUMED_CLAIMED_COVER_INDEX = `
|
|
220
259
|
DROP INDEX IF EXISTS idx_hmsh_esc_claimed_cover`;
|
|
221
|
-
// ── The
|
|
222
|
-
/** Reports existence + validity so a crash-orphaned INVALID build is rebuilt. */
|
|
223
|
-
const RESOLVED_COVER_INDEX_STATE = `
|
|
224
|
-
SELECT i.indisvalid AS valid
|
|
225
|
-
FROM pg_class c
|
|
226
|
-
JOIN pg_index i ON i.indexrelid = c.oid
|
|
227
|
-
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
228
|
-
WHERE c.relname = 'idx_hmsh_esc_resolved_cover' AND n.nspname = 'public'`;
|
|
260
|
+
// ── The app-layer indexes (built via ensureAppIndex) ────────────────────────
|
|
229
261
|
const ENSURE_RESOLVED_COVER_INDEX = `\
|
|
230
262
|
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_hmsh_esc_resolved_cover
|
|
231
263
|
ON public.hmsh_escalations (role, resolved_at DESC, claimed_at, created_at)
|
|
232
264
|
WHERE status = 'resolved'`;
|
|
265
|
+
const ENSURE_ENDED_AT_INDEX = `\
|
|
266
|
+
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_hmsh_esc_ended_at
|
|
267
|
+
ON public.hmsh_escalations ((COALESCE(resolved_at, updated_at)))
|
|
268
|
+
WHERE status <> 'pending'`;
|
|
@@ -22,6 +22,10 @@ exports.FACET_KEY = /^[a-zA-Z0-9_]+$/;
|
|
|
22
22
|
function metaExpr(key, numeric = false) {
|
|
23
23
|
return numeric ? `(metadata->>'${key}')::numeric` : `(metadata->>'${key}')`;
|
|
24
24
|
}
|
|
25
|
+
/** Escape LIKE wildcards so a prefix VALUE is matched literally. */
|
|
26
|
+
function escapeLikePrefix(value) {
|
|
27
|
+
return value.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
28
|
+
}
|
|
25
29
|
/**
|
|
26
30
|
* Build the WHERE clause for a faceted query, pushing bound params into `params`.
|
|
27
31
|
* Every value is parameterized; only validated column names / metadata keys and
|
|
@@ -51,10 +55,23 @@ function buildFacetWhere(q, params, opts = {}) {
|
|
|
51
55
|
params.push(JSON.stringify(q.facets));
|
|
52
56
|
clauses.push(`metadata @> $${params.length}::jsonb`); // GIN-served
|
|
53
57
|
}
|
|
58
|
+
if (q.anyOf && q.anyOf.length) {
|
|
59
|
+
params.push(q.anyOf.map((a) => JSON.stringify(a)));
|
|
60
|
+
clauses.push(`metadata @> ANY($${params.length}::jsonb[])`); // GIN-served OR
|
|
61
|
+
}
|
|
54
62
|
if (q.block && q.block.length) {
|
|
55
63
|
params.push(q.block.map((b) => JSON.stringify(b)));
|
|
56
64
|
clauses.push(`NOT (metadata @> ANY($${params.length}::jsonb[]))`);
|
|
57
65
|
}
|
|
66
|
+
// Case-insensitive prefix on facet VALUES — the locate affordance. No
|
|
67
|
+
// dedicated index by design: the predicate composes into scans the rest of
|
|
68
|
+
// the filter already bounds (the live set, or an analytics window).
|
|
69
|
+
for (const [key, value] of Object.entries(q.prefix ?? {})) {
|
|
70
|
+
if (!exports.FACET_KEY.test(key) || typeof value !== 'string' || !value)
|
|
71
|
+
continue;
|
|
72
|
+
params.push(`${escapeLikePrefix(value)}%`);
|
|
73
|
+
clauses.push(`${metaExpr(key)} ILIKE $${params.length}`);
|
|
74
|
+
}
|
|
58
75
|
for (const r of q.range ?? []) {
|
|
59
76
|
if (!exports.FACET_KEY.test(r.facet) || !RANGE_OPS.has(r.op))
|
|
60
77
|
continue;
|
|
@@ -5,3 +5,5 @@ export * from './queries';
|
|
|
5
5
|
export * from './resolver-validation';
|
|
6
6
|
export { ensureEscalationCompatView } from './client';
|
|
7
7
|
export { ensureFacetReady, searchByFacets, searchGroups, countByFacets, claimGroups, claimByFacets, } from './facets';
|
|
8
|
+
export { aggregateByFacets, timelineByFacet, resolveEntitySystem, resetAnalyticsCaches, } from './aggregates';
|
|
9
|
+
export { AnalyticsInputError } from './aggregate-validate';
|
|
@@ -14,7 +14,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
14
14
|
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
15
|
};
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
-
exports.claimByFacets = exports.claimGroups = exports.countByFacets = exports.searchGroups = exports.searchByFacets = exports.ensureFacetReady = exports.ensureEscalationCompatView = void 0;
|
|
17
|
+
exports.AnalyticsInputError = exports.resetAnalyticsCaches = exports.resolveEntitySystem = exports.timelineByFacet = exports.aggregateByFacets = exports.claimByFacets = exports.claimGroups = exports.countByFacets = exports.searchGroups = exports.searchByFacets = exports.ensureFacetReady = exports.ensureEscalationCompatView = void 0;
|
|
18
18
|
__exportStar(require("./types"), exports);
|
|
19
19
|
__exportStar(require("./crud"), exports);
|
|
20
20
|
__exportStar(require("./bulk"), exports);
|
|
@@ -29,3 +29,10 @@ Object.defineProperty(exports, "searchGroups", { enumerable: true, get: function
|
|
|
29
29
|
Object.defineProperty(exports, "countByFacets", { enumerable: true, get: function () { return facets_1.countByFacets; } });
|
|
30
30
|
Object.defineProperty(exports, "claimGroups", { enumerable: true, get: function () { return facets_1.claimGroups; } });
|
|
31
31
|
Object.defineProperty(exports, "claimByFacets", { enumerable: true, get: function () { return facets_1.claimByFacets; } });
|
|
32
|
+
var aggregates_1 = require("./aggregates");
|
|
33
|
+
Object.defineProperty(exports, "aggregateByFacets", { enumerable: true, get: function () { return aggregates_1.aggregateByFacets; } });
|
|
34
|
+
Object.defineProperty(exports, "timelineByFacet", { enumerable: true, get: function () { return aggregates_1.timelineByFacet; } });
|
|
35
|
+
Object.defineProperty(exports, "resolveEntitySystem", { enumerable: true, get: function () { return aggregates_1.resolveEntitySystem; } });
|
|
36
|
+
Object.defineProperty(exports, "resetAnalyticsCaches", { enumerable: true, get: function () { return aggregates_1.resetAnalyticsCaches; } });
|
|
37
|
+
var aggregate_validate_1 = require("./aggregate-validate");
|
|
38
|
+
Object.defineProperty(exports, "AnalyticsInputError", { enumerable: true, get: function () { return aggregate_validate_1.AnalyticsInputError; } });
|
|
@@ -12,3 +12,21 @@ export declare class TtlCache<T> {
|
|
|
12
12
|
resolve(key: string, compute: () => Promise<T>): Promise<T>;
|
|
13
13
|
clear(): void;
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* TtlCache with an LRU bound — for key spaces the caller doesn't control.
|
|
17
|
+
*
|
|
18
|
+
* The analytics caches key by a canonical stringify of the whole query input,
|
|
19
|
+
* so distinct keys are unbounded; the expired-entry sweep alone can't bound
|
|
20
|
+
* memory the way it does for TtlCache's small role-set × period key space.
|
|
21
|
+
* Same single-flight and reject-evict semantics; a hit refreshes recency
|
|
22
|
+
* (Map delete+set, the enforcement-cache pattern), and inserting past
|
|
23
|
+
* `maxEntries` evicts the least-recently-used entry.
|
|
24
|
+
*/
|
|
25
|
+
export declare class BoundedTtlCache<T> {
|
|
26
|
+
private readonly ttlMs;
|
|
27
|
+
private readonly maxEntries;
|
|
28
|
+
private readonly store;
|
|
29
|
+
constructor(ttlMs: number, maxEntries: number);
|
|
30
|
+
resolve(key: string, compute: () => Promise<T>): Promise<T>;
|
|
31
|
+
clear(): void;
|
|
32
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// container drift within the TTL is acceptable. No cross-node coordination.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
14
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
exports.TtlCache = void 0;
|
|
15
|
+
exports.BoundedTtlCache = exports.TtlCache = void 0;
|
|
16
16
|
class TtlCache {
|
|
17
17
|
constructor(ttlMs) {
|
|
18
18
|
this.ttlMs = ttlMs;
|
|
@@ -51,3 +51,49 @@ class TtlCache {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
exports.TtlCache = TtlCache;
|
|
54
|
+
/**
|
|
55
|
+
* TtlCache with an LRU bound — for key spaces the caller doesn't control.
|
|
56
|
+
*
|
|
57
|
+
* The analytics caches key by a canonical stringify of the whole query input,
|
|
58
|
+
* so distinct keys are unbounded; the expired-entry sweep alone can't bound
|
|
59
|
+
* memory the way it does for TtlCache's small role-set × period key space.
|
|
60
|
+
* Same single-flight and reject-evict semantics; a hit refreshes recency
|
|
61
|
+
* (Map delete+set, the enforcement-cache pattern), and inserting past
|
|
62
|
+
* `maxEntries` evicts the least-recently-used entry.
|
|
63
|
+
*/
|
|
64
|
+
class BoundedTtlCache {
|
|
65
|
+
constructor(ttlMs, maxEntries) {
|
|
66
|
+
this.ttlMs = ttlMs;
|
|
67
|
+
this.maxEntries = maxEntries;
|
|
68
|
+
this.store = new Map();
|
|
69
|
+
}
|
|
70
|
+
async resolve(key, compute) {
|
|
71
|
+
const hit = this.store.get(key);
|
|
72
|
+
if (hit && Date.now() - hit.at < this.ttlMs) {
|
|
73
|
+
this.store.delete(key); // refresh recency
|
|
74
|
+
this.store.set(key, hit);
|
|
75
|
+
return hit.value;
|
|
76
|
+
}
|
|
77
|
+
for (const [k, entry] of this.store) {
|
|
78
|
+
if (Date.now() - entry.at >= this.ttlMs)
|
|
79
|
+
this.store.delete(k);
|
|
80
|
+
}
|
|
81
|
+
while (this.store.size >= this.maxEntries) {
|
|
82
|
+
const oldest = this.store.keys().next().value;
|
|
83
|
+
this.store.delete(oldest);
|
|
84
|
+
}
|
|
85
|
+
const value = compute();
|
|
86
|
+
this.store.set(key, { at: Date.now(), value });
|
|
87
|
+
try {
|
|
88
|
+
return await value;
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
this.store.delete(key);
|
|
92
|
+
throw err;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
clear() {
|
|
96
|
+
this.store.clear();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
exports.BoundedTtlCache = BoundedTtlCache;
|
|
@@ -17,6 +17,7 @@ const facet_sql_1 = require("./facet-sql");
|
|
|
17
17
|
const types_1 = require("./types");
|
|
18
18
|
const sql_1 = require("./sql");
|
|
19
19
|
const metrics_cache_1 = require("./metrics-cache");
|
|
20
|
+
const aggregates_1 = require("./aggregates");
|
|
20
21
|
/**
|
|
21
22
|
* Default sort is priority ASC, created_at ASC. A user-chosen `sort_by` maps to
|
|
22
23
|
* a single column (DESC unless `order='asc'`), matching the legacy behavior.
|
|
@@ -380,6 +381,7 @@ const stationPeriodCache = new metrics_cache_1.TtlCache(STATION_PERIOD_CACHE_TTL
|
|
|
380
381
|
function invalidateEscalationAggregates() {
|
|
381
382
|
stationPeriodCache.clear();
|
|
382
383
|
escalationStatsCache.clear();
|
|
384
|
+
(0, aggregates_1.clearNowAnchoredAnalyticsCache)();
|
|
383
385
|
}
|
|
384
386
|
/** Test hook: drop cached period metrics AND escalation stats so tests observe fresh rows. */
|
|
385
387
|
function resetStationMetricsCache() {
|
|
@@ -23,6 +23,6 @@ export interface ResolverSchemaViolationReport {
|
|
|
23
23
|
* surface's task fallback); by default the row's own envelope column is used.
|
|
24
24
|
* It is only invoked when the role actually enforces.
|
|
25
25
|
*/
|
|
26
|
-
export declare function checkResolverPayload(escalation: ResolvableEscalationRow, resolverPayload: Record<string, any>, loadEnvelope?: () => Promise<Record<string, any
|
|
26
|
+
export declare function checkResolverPayload(escalation: ResolvableEscalationRow, resolverPayload: Record<string, any>, loadEnvelope?: () => Promise<Record<string, any>>, resolvingUserId?: string): Promise<ResolverSchemaViolationReport | null>;
|
|
27
27
|
/** The canonical 422 body for a violation report (types/validation.ts). */
|
|
28
28
|
export declare function toValidationErrorBody(report: ResolverSchemaViolationReport): LTValidationErrorBody;
|
|
@@ -22,6 +22,7 @@ exports.toValidationErrorBody = toValidationErrorBody;
|
|
|
22
22
|
* the gate passes.
|
|
23
23
|
*/
|
|
24
24
|
const form_validation_1 = require("../../shared/form-validation");
|
|
25
|
+
const submit_guard_1 = require("./submit-guard");
|
|
25
26
|
const enforcement_cache_1 = require("../role/enforcement-cache");
|
|
26
27
|
const escalation_1 = require("../../types/escalation");
|
|
27
28
|
const validation_1 = require("../../types/validation");
|
|
@@ -54,7 +55,7 @@ function pinnedVersionOf(metadata) {
|
|
|
54
55
|
* surface's task fallback); by default the row's own envelope column is used.
|
|
55
56
|
* It is only invoked when the role actually enforces.
|
|
56
57
|
*/
|
|
57
|
-
async function checkResolverPayload(escalation, resolverPayload, loadEnvelope) {
|
|
58
|
+
async function checkResolverPayload(escalation, resolverPayload, loadEnvelope, resolvingUserId) {
|
|
58
59
|
const role = escalation.role;
|
|
59
60
|
if (!role)
|
|
60
61
|
return null;
|
|
@@ -72,15 +73,21 @@ async function checkResolverPayload(escalation, resolverPayload, loadEnvelope) {
|
|
|
72
73
|
const envelope = loadEnvelope
|
|
73
74
|
? await loadEnvelope()
|
|
74
75
|
: parseJsonOrNull(escalation.envelope) ?? {};
|
|
75
|
-
const
|
|
76
|
+
const ctx = {
|
|
76
77
|
escalation: escalation,
|
|
77
78
|
metadata,
|
|
78
79
|
envelope,
|
|
79
80
|
payload: parseJsonOrNull(escalation.escalation_payload),
|
|
80
|
-
}
|
|
81
|
-
|
|
81
|
+
};
|
|
82
|
+
const violations = (0, form_validation_1.validateResolverPayload)(schema, resolverPayload, ctx);
|
|
83
|
+
// The query precondition (x-lt-submit-guard): reject while the embedded query
|
|
84
|
+
// still returns rows. Enforced on the same enforce_schema opt-in as the field
|
|
85
|
+
// contract, so a raw-API resolve honors the gate the dashboard shows.
|
|
86
|
+
const guardViolation = await (0, submit_guard_1.checkSubmitGuard)(schema, ctx, resolvingUserId);
|
|
87
|
+
const allViolations = guardViolation ? [...violations, guardViolation] : violations;
|
|
88
|
+
if (allViolations.length === 0)
|
|
82
89
|
return null;
|
|
83
|
-
return { role, schemaVersion: pin, violations };
|
|
90
|
+
return { role, schemaVersion: pin, violations: allViolations };
|
|
84
91
|
}
|
|
85
92
|
/** The canonical 422 body for a violation report (types/validation.ts). */
|
|
86
93
|
function toValidationErrorBody(report) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side enforcement of the `x-lt-submit-guard` query precondition — the
|
|
3
|
+
* API-first counterpart of the dashboard's `useSubmitGuard`. Before an
|
|
4
|
+
* escalation resolves, if its form schema declares a submit guard, the same
|
|
5
|
+
* embedded query runs and the resolve is REJECTED while it still returns rows.
|
|
6
|
+
*
|
|
7
|
+
* Isomorphism with the dashboard is exact: the guard reuses `resolveScopedQuery`
|
|
8
|
+
* (the mapping the escalation-list embed uses) and runs it through
|
|
9
|
+
* `searchEscalationsFaceted` — the same faceted query the list API composes.
|
|
10
|
+
*
|
|
11
|
+
* Two deliberate rules:
|
|
12
|
+
* - The count is taken with `global: true`, bypassing the resolver's read
|
|
13
|
+
* scope, so a role that cannot SEE the children can never falsely clear the
|
|
14
|
+
* gate. Only a real, empty result count clears it (the "403 ≠ empty" rule).
|
|
15
|
+
* - A guard whose scope cannot be evaluated (e.g. `assigned:'me'` on a surface
|
|
16
|
+
* with no resolving user, like an MCP resolve) is skipped here; the consuming
|
|
17
|
+
* workflow's own verification remains the durable backstop.
|
|
18
|
+
*/
|
|
19
|
+
import { type ShowIfContext } from '../../shared/form-validation';
|
|
20
|
+
import type { LTFieldViolation } from '../../types/validation';
|
|
21
|
+
/** The synthetic violation field for a guard rejection — not a form field. */
|
|
22
|
+
export declare const SUBMIT_GUARD_VIOLATION_FIELD = "_submitGuard";
|
|
23
|
+
/**
|
|
24
|
+
* Returns a violation when the escalation's submit guard still matches rows, or
|
|
25
|
+
* null when there is no guard, its scope is inert, or the query is confirmed
|
|
26
|
+
* empty. The context carries the escalation domains the query facets and the
|
|
27
|
+
* message interpolate against.
|
|
28
|
+
*/
|
|
29
|
+
export declare function checkSubmitGuard(schema: Record<string, any> | null | undefined, ctx: ShowIfContext, resolvingUserId: string | undefined): Promise<LTFieldViolation | null>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SUBMIT_GUARD_VIOLATION_FIELD = void 0;
|
|
4
|
+
exports.checkSubmitGuard = checkSubmitGuard;
|
|
5
|
+
/**
|
|
6
|
+
* Server-side enforcement of the `x-lt-submit-guard` query precondition — the
|
|
7
|
+
* API-first counterpart of the dashboard's `useSubmitGuard`. Before an
|
|
8
|
+
* escalation resolves, if its form schema declares a submit guard, the same
|
|
9
|
+
* embedded query runs and the resolve is REJECTED while it still returns rows.
|
|
10
|
+
*
|
|
11
|
+
* Isomorphism with the dashboard is exact: the guard reuses `resolveScopedQuery`
|
|
12
|
+
* (the mapping the escalation-list embed uses) and runs it through
|
|
13
|
+
* `searchEscalationsFaceted` — the same faceted query the list API composes.
|
|
14
|
+
*
|
|
15
|
+
* Two deliberate rules:
|
|
16
|
+
* - The count is taken with `global: true`, bypassing the resolver's read
|
|
17
|
+
* scope, so a role that cannot SEE the children can never falsely clear the
|
|
18
|
+
* gate. Only a real, empty result count clears it (the "403 ≠ empty" rule).
|
|
19
|
+
* - A guard whose scope cannot be evaluated (e.g. `assigned:'me'` on a surface
|
|
20
|
+
* with no resolving user, like an MCP resolve) is skipped here; the consuming
|
|
21
|
+
* workflow's own verification remains the durable backstop.
|
|
22
|
+
*/
|
|
23
|
+
const form_validation_1 = require("../../shared/form-validation");
|
|
24
|
+
const queries_1 = require("./queries");
|
|
25
|
+
/** The synthetic violation field for a guard rejection — not a form field. */
|
|
26
|
+
exports.SUBMIT_GUARD_VIOLATION_FIELD = '_submitGuard';
|
|
27
|
+
/**
|
|
28
|
+
* Returns a violation when the escalation's submit guard still matches rows, or
|
|
29
|
+
* null when there is no guard, its scope is inert, or the query is confirmed
|
|
30
|
+
* empty. The context carries the escalation domains the query facets and the
|
|
31
|
+
* message interpolate against.
|
|
32
|
+
*/
|
|
33
|
+
async function checkSubmitGuard(schema, ctx, resolvingUserId) {
|
|
34
|
+
const guard = (0, form_validation_1.readSubmitGuard)(schema);
|
|
35
|
+
if (!guard || guard.mustBeEmpty === false)
|
|
36
|
+
return null;
|
|
37
|
+
const scoped = (0, form_validation_1.resolveScopedQuery)(guard.query, ctx, resolvingUserId);
|
|
38
|
+
if (!scoped.enabled)
|
|
39
|
+
return null;
|
|
40
|
+
const { total } = await (0, queries_1.searchEscalationsFaceted)({
|
|
41
|
+
global: true,
|
|
42
|
+
facet: {
|
|
43
|
+
role: scoped.role,
|
|
44
|
+
status: scoped.status,
|
|
45
|
+
available: scoped.available,
|
|
46
|
+
facets: scoped.facets,
|
|
47
|
+
},
|
|
48
|
+
assigned_to: scoped.assigned_to,
|
|
49
|
+
limit: 1,
|
|
50
|
+
offset: 0,
|
|
51
|
+
});
|
|
52
|
+
if (total <= 0)
|
|
53
|
+
return null;
|
|
54
|
+
const template = guard.message ?? form_validation_1.SUBMIT_GUARD_DEFAULT_MESSAGE;
|
|
55
|
+
const message = (0, form_validation_1.interpolateHelp)(template.replace(/\{\{count\}\}/g, String(total)), ctx);
|
|
56
|
+
return { field: exports.SUBMIT_GUARD_VIOLATION_FIELD, message };
|
|
57
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { LTApiAuth } from '../../types/sdk';
|
|
2
|
+
/** The acting-auth resolution result: an auth to act as, or the loud reason not to. */
|
|
3
|
+
export type ActingAuthResult = {
|
|
4
|
+
ok: true;
|
|
5
|
+
auth: LTApiAuth;
|
|
6
|
+
} | {
|
|
7
|
+
ok: false;
|
|
8
|
+
error: string;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Exchange a supplied acting grant for the person it names. A supplied token
|
|
12
|
+
* that fails — wrong label, expired, exhausted, or a vanished user — is a
|
|
13
|
+
* terminal error, never a silent fall-back to the session identity: that
|
|
14
|
+
* would misattribute whatever follows.
|
|
15
|
+
*/
|
|
16
|
+
export declare function resolveActingAuth(actingToken: string): Promise<ActingAuthResult>;
|