@way_marks/server 0.9.0 → 1.0.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/dist/api/server.js +598 -0
- package/dist/approval/manager.js +249 -0
- package/dist/approval/manager.test.js +315 -0
- package/dist/db/database.js +544 -1
- package/dist/escalation/manager.js +205 -0
- package/dist/escalation/manager.test.js +519 -0
- package/dist/notifications/service.js +457 -0
- package/dist/policy/engine.js +420 -0
- package/dist/remediation/recommender.js +309 -0
- package/dist/risk/analyzer.js +442 -0
- package/dist/risk/analyzer.test.js +482 -0
- package/dist/rollback/blocker.js +222 -0
- package/dist/rollback/manager.js +245 -0
- package/dist/rollback/manager.test.js +552 -0
- package/package.json +1 -1
- package/src/ui/index.html +862 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Escalation Manager — Core logic for approval escalation
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Check for stalled approval requests
|
|
7
|
+
* - Create escalation requests when approvals timeout
|
|
8
|
+
* - Determine escalation targets
|
|
9
|
+
* - Process escalation decisions
|
|
10
|
+
* - Track escalation history and audit trail
|
|
11
|
+
*
|
|
12
|
+
* Integration with Phase 2 Approval System:
|
|
13
|
+
* - Monitors approval_requests for timeout
|
|
14
|
+
* - Creates escalation_requests when deadline passed
|
|
15
|
+
* - Prevents rollback if escalation is blocked
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.checkAndEscalateStaleApprovals = checkAndEscalateStaleApprovals;
|
|
19
|
+
exports.determineEscalationTargets = determineEscalationTargets;
|
|
20
|
+
exports.submitEscalationDecision = submitEscalationDecision;
|
|
21
|
+
exports.getEscalationStatus = getEscalationStatus;
|
|
22
|
+
exports.canProceedWithRollbackAfterEscalation = canProceedWithRollbackAfterEscalation;
|
|
23
|
+
exports.getEscalationHistoryForSession = getEscalationHistoryForSession;
|
|
24
|
+
exports.startEscalationScheduler = startEscalationScheduler;
|
|
25
|
+
exports.stopEscalationScheduler = stopEscalationScheduler;
|
|
26
|
+
exports.isSchedulerRunning = isSchedulerRunning;
|
|
27
|
+
const database_1 = require("../db/database");
|
|
28
|
+
/**
|
|
29
|
+
* Check for stalled approval requests and trigger escalations
|
|
30
|
+
* Called periodically by scheduler (e.g., every minute)
|
|
31
|
+
*/
|
|
32
|
+
function checkAndEscalateStaleApprovals() {
|
|
33
|
+
const now = new Date().toISOString();
|
|
34
|
+
const staleApprovals = (0, database_1.getStaleApprovals)(now);
|
|
35
|
+
const rules = (0, database_1.getAllEscalationRules)();
|
|
36
|
+
const escalationsCreated = [];
|
|
37
|
+
for (const approval of staleApprovals) {
|
|
38
|
+
// For now, use default rule (escalate to all-targets rule if exists)
|
|
39
|
+
// In future, could match escalation rules based on approval attributes
|
|
40
|
+
const rule = rules.length > 0 ? rules[0] : null;
|
|
41
|
+
if (!rule)
|
|
42
|
+
continue;
|
|
43
|
+
const escalationTargets = JSON.parse(rule.escalation_targets);
|
|
44
|
+
const deadline = new Date(now);
|
|
45
|
+
deadline.setHours(deadline.getHours() + rule.timeout_hours);
|
|
46
|
+
const requestId = `escalation-${approval.approval_request_id}-${Date.now()}`;
|
|
47
|
+
try {
|
|
48
|
+
(0, database_1.createEscalationRequest)(requestId, approval.approval_request_id, approval.session_id, escalationTargets, deadline.toISOString());
|
|
49
|
+
escalationsCreated.push(requestId);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.error(`Failed to create escalation for ${approval.approval_request_id}:`, err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return {
|
|
56
|
+
hasStaleApprovals: staleApprovals.length > 0,
|
|
57
|
+
staleApprovals,
|
|
58
|
+
escalationsCreated,
|
|
59
|
+
timestamp: now,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Determine escalation targets for a given approval request
|
|
64
|
+
*/
|
|
65
|
+
function determineEscalationTargets(approval_request_id) {
|
|
66
|
+
const rules = (0, database_1.getAllEscalationRules)();
|
|
67
|
+
// Simple logic: use first active rule's targets
|
|
68
|
+
// In future, could match based on approval attributes (session type, user, etc)
|
|
69
|
+
if (rules.length > 0) {
|
|
70
|
+
return JSON.parse(rules[0].escalation_targets);
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Submit escalation decision
|
|
76
|
+
*/
|
|
77
|
+
function submitEscalationDecision(escalation_request_id, target_id, decision, reason) {
|
|
78
|
+
const escalation = (0, database_1.getEscalationRequest)(escalation_request_id);
|
|
79
|
+
if (!escalation) {
|
|
80
|
+
throw new Error(`Escalation request ${escalation_request_id} not found`);
|
|
81
|
+
}
|
|
82
|
+
const targets = JSON.parse(escalation.escalation_targets);
|
|
83
|
+
if (!targets.includes(target_id)) {
|
|
84
|
+
throw new Error(`${target_id} is not authorized to decide on this escalation`);
|
|
85
|
+
}
|
|
86
|
+
// Check if already decided by this target
|
|
87
|
+
const existingDecisions = (0, database_1.getEscalationDecisions)(escalation_request_id);
|
|
88
|
+
if (existingDecisions.some(d => d.target_id === target_id)) {
|
|
89
|
+
throw new Error(`${target_id} has already made a decision on this escalation`);
|
|
90
|
+
}
|
|
91
|
+
// Record decision
|
|
92
|
+
const decision_id = `escalation-decision-${escalation_request_id}-${target_id}-${Date.now()}`;
|
|
93
|
+
(0, database_1.submitEscalationDecision)(decision_id, escalation_request_id, target_id, decision, reason);
|
|
94
|
+
// Return updated status
|
|
95
|
+
return getEscalationStatus(escalation_request_id);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Get escalation status
|
|
99
|
+
*/
|
|
100
|
+
function getEscalationStatus(escalation_request_id) {
|
|
101
|
+
const escalation = (0, database_1.getEscalationRequest)(escalation_request_id);
|
|
102
|
+
if (!escalation) {
|
|
103
|
+
throw new Error(`Escalation request ${escalation_request_id} not found`);
|
|
104
|
+
}
|
|
105
|
+
const decisions = (0, database_1.getEscalationDecisions)(escalation_request_id);
|
|
106
|
+
const targets = JSON.parse(escalation.escalation_targets);
|
|
107
|
+
const blockDecisions = decisions.filter(d => d.decision === 'block');
|
|
108
|
+
const proceedDecisions = decisions.filter(d => d.decision === 'proceed');
|
|
109
|
+
// Status logic:
|
|
110
|
+
// - If any "block" → blocked
|
|
111
|
+
// - If all targets have decided with no blocks → proceeded
|
|
112
|
+
// - Otherwise → pending
|
|
113
|
+
let status = 'pending';
|
|
114
|
+
if (blockDecisions.length > 0) {
|
|
115
|
+
status = 'blocked';
|
|
116
|
+
}
|
|
117
|
+
else if (proceedDecisions.length === targets.length) {
|
|
118
|
+
status = 'proceeded';
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
request_id: escalation_request_id,
|
|
122
|
+
status,
|
|
123
|
+
escalation_triggered_at: escalation.escalation_triggered_at || new Date().toISOString(),
|
|
124
|
+
escalation_deadline: escalation.escalation_deadline,
|
|
125
|
+
targets_count: targets.length,
|
|
126
|
+
decisions_received: decisions.length,
|
|
127
|
+
decisions: decisions.map(d => ({
|
|
128
|
+
target_id: d.target_id,
|
|
129
|
+
decision: d.decision,
|
|
130
|
+
reason: d.reason ?? undefined,
|
|
131
|
+
})),
|
|
132
|
+
can_proceed: status === 'proceeded',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Check if rollback can proceed given escalation status
|
|
137
|
+
* If escalation exists and is blocked, prevent rollback
|
|
138
|
+
*/
|
|
139
|
+
function canProceedWithRollbackAfterEscalation(approval_request_id) {
|
|
140
|
+
const pending = (0, database_1.getPendingEscalations)();
|
|
141
|
+
const escalation = pending.find(e => e.approval_request_id === approval_request_id);
|
|
142
|
+
if (!escalation) {
|
|
143
|
+
// No pending escalation → can proceed
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// Check escalation status
|
|
147
|
+
try {
|
|
148
|
+
const status = getEscalationStatus(escalation.request_id);
|
|
149
|
+
return status.can_proceed;
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// If error getting status, be conservative and block
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Get escalation history for a session
|
|
158
|
+
*/
|
|
159
|
+
function getEscalationHistoryForSession(session_id) {
|
|
160
|
+
return (0, database_1.getEscalationHistory)(session_id);
|
|
161
|
+
}
|
|
162
|
+
let schedulerHandle = null;
|
|
163
|
+
function startEscalationScheduler(config = { interval_ms: 60000, enabled: true }) {
|
|
164
|
+
if (!config.enabled) {
|
|
165
|
+
console.log('[Escalation] Scheduler disabled in config');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (schedulerHandle) {
|
|
169
|
+
console.log('[Escalation] Scheduler already running');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`[Escalation] Starting scheduler (check every ${config.interval_ms}ms)`);
|
|
173
|
+
schedulerHandle = setInterval(() => {
|
|
174
|
+
try {
|
|
175
|
+
const result = checkAndEscalateStaleApprovals();
|
|
176
|
+
if (result.escalationsCreated.length > 0) {
|
|
177
|
+
console.log(`[Escalation] Created ${result.escalationsCreated.length} escalation requests`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
console.error('[Escalation] Scheduler error:', err);
|
|
182
|
+
}
|
|
183
|
+
}, config.interval_ms);
|
|
184
|
+
}
|
|
185
|
+
function stopEscalationScheduler() {
|
|
186
|
+
if (schedulerHandle) {
|
|
187
|
+
clearInterval(schedulerHandle);
|
|
188
|
+
schedulerHandle = null;
|
|
189
|
+
console.log('[Escalation] Scheduler stopped');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
function isSchedulerRunning() {
|
|
193
|
+
return schedulerHandle !== null;
|
|
194
|
+
}
|
|
195
|
+
exports.default = {
|
|
196
|
+
checkAndEscalateStaleApprovals,
|
|
197
|
+
determineEscalationTargets,
|
|
198
|
+
submitEscalationDecision,
|
|
199
|
+
getEscalationStatus,
|
|
200
|
+
canProceedWithRollbackAfterEscalation,
|
|
201
|
+
getEscalationHistoryForSession,
|
|
202
|
+
startEscalationScheduler,
|
|
203
|
+
stopEscalationScheduler,
|
|
204
|
+
isSchedulerRunning,
|
|
205
|
+
};
|