@way_marks/server 0.9.0 → 2.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/db/database.js +754 -185
- package/dist/escalation/manager.js +205 -0
- package/dist/notifications/service.js +457 -0
- package/dist/policies/engine.js +11 -5
- package/dist/policy/engine.js +420 -0
- package/dist/remediation/recommender.js +309 -0
- package/dist/risk/analyzer.js +453 -0
- package/dist/rollback/blocker.js +222 -0
- package/dist/rollback/manager.js +245 -0
- package/package.json +1 -1
- package/src/ui/index.html +862 -0
- package/dist/approvals/handler.test.js +0 -172
- package/dist/policies/engine.test.js +0 -241
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Approval Manager — Core logic for team approval routing
|
|
4
|
+
*
|
|
5
|
+
* Responsibilities:
|
|
6
|
+
* - Determine which approvers are required for a given session rollback
|
|
7
|
+
* - Create approval requests (pending approvals)
|
|
8
|
+
* - Process approval decisions (approve/reject)
|
|
9
|
+
* - Check if all required approvals are satisfied
|
|
10
|
+
* - Track approval history and audit trail
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.determineRequiredApprovers = determineRequiredApprovers;
|
|
14
|
+
exports.createApprovalRequestForSession = createApprovalRequestForSession;
|
|
15
|
+
exports.submitApprovalDecision = submitApprovalDecision;
|
|
16
|
+
exports.getApprovalStatus = getApprovalStatus;
|
|
17
|
+
exports.canProceedWithRollback = canProceedWithRollback;
|
|
18
|
+
exports.getPendingApprovalsForUser = getPendingApprovalsForUser;
|
|
19
|
+
exports.getApprovalHistory = getApprovalHistory;
|
|
20
|
+
const database_1 = require("../db/database");
|
|
21
|
+
/**
|
|
22
|
+
* Determine if a session rollback requires approval based on routes
|
|
23
|
+
*/
|
|
24
|
+
function determineRequiredApprovers(session, actions) {
|
|
25
|
+
const routes = (0, database_1.getAllApprovalRoutes)();
|
|
26
|
+
// Collect all unique approvers from matching routes
|
|
27
|
+
const requiredApprovers = new Set();
|
|
28
|
+
const matchedRoutes = [];
|
|
29
|
+
for (const route of routes) {
|
|
30
|
+
if (routeMatches(route, session, actions)) {
|
|
31
|
+
matchedRoutes.push(route);
|
|
32
|
+
const approvers = JSON.parse(route.approver_ids);
|
|
33
|
+
approvers.forEach(a => requiredApprovers.add(a));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const approverArray = Array.from(requiredApprovers);
|
|
37
|
+
const requiresApproval = approverArray.length > 0;
|
|
38
|
+
return {
|
|
39
|
+
requiresApproval,
|
|
40
|
+
requiredApprovers: approverArray,
|
|
41
|
+
routes: matchedRoutes,
|
|
42
|
+
reason: requiresApproval
|
|
43
|
+
? `${matchedRoutes.length} approval route(s) matched`
|
|
44
|
+
: 'No approval routes matched',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Check if an approval route matches the given session
|
|
49
|
+
*/
|
|
50
|
+
function routeMatches(route, session, actions) {
|
|
51
|
+
const condition_type = route.condition_type || 'all_sessions';
|
|
52
|
+
switch (condition_type) {
|
|
53
|
+
case 'all_sessions':
|
|
54
|
+
return true;
|
|
55
|
+
case 'tool_name':
|
|
56
|
+
// Match if any action uses specified tool
|
|
57
|
+
if (!route.condition_json)
|
|
58
|
+
return false;
|
|
59
|
+
try {
|
|
60
|
+
const condition = JSON.parse(route.condition_json);
|
|
61
|
+
return actions.some(a => a.tool_name === condition.value);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
case 'action_count':
|
|
67
|
+
// Match if action count exceeds threshold
|
|
68
|
+
if (!route.condition_json)
|
|
69
|
+
return false;
|
|
70
|
+
try {
|
|
71
|
+
const condition = JSON.parse(route.condition_json);
|
|
72
|
+
const threshold = condition.value || 5;
|
|
73
|
+
return actions.length >= threshold;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
case 'risk_level':
|
|
79
|
+
// Match if any action is flagged as high-risk
|
|
80
|
+
if (!route.condition_json)
|
|
81
|
+
return false;
|
|
82
|
+
try {
|
|
83
|
+
const condition = JSON.parse(route.condition_json);
|
|
84
|
+
// Check if any action has is_reversible = 0 (irreversible = high risk)
|
|
85
|
+
return actions.some(a => a.is_reversible === 0);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
default:
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Create an approval request for a session rollback
|
|
96
|
+
* Returns request_id and list of required approvers
|
|
97
|
+
*/
|
|
98
|
+
function createApprovalRequestForSession(session_id, session, actions, triggered_by) {
|
|
99
|
+
try {
|
|
100
|
+
const check = determineRequiredApprovers(session, actions);
|
|
101
|
+
if (!check.requiresApproval) {
|
|
102
|
+
// No approval needed
|
|
103
|
+
return {
|
|
104
|
+
success: true,
|
|
105
|
+
request_id: '', // No request created
|
|
106
|
+
requires_approval: false,
|
|
107
|
+
required_approvers: [],
|
|
108
|
+
pending_approvers: [],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
// Create approval request
|
|
112
|
+
const request_id = `approval-${session_id}-${Date.now()}`;
|
|
113
|
+
const route_id = check.routes[0].route_id; // Use first matching route
|
|
114
|
+
(0, database_1.createApprovalRequest)(request_id, session_id, route_id, triggered_by, check.requiredApprovers);
|
|
115
|
+
return {
|
|
116
|
+
success: true,
|
|
117
|
+
request_id,
|
|
118
|
+
requires_approval: true,
|
|
119
|
+
required_approvers: check.requiredApprovers,
|
|
120
|
+
pending_approvers: check.requiredApprovers, // All are pending initially
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return {
|
|
125
|
+
success: false,
|
|
126
|
+
request_id: '',
|
|
127
|
+
requires_approval: false,
|
|
128
|
+
required_approvers: [],
|
|
129
|
+
pending_approvers: [],
|
|
130
|
+
error: error.message,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Submit an approval decision (approve or reject)
|
|
136
|
+
*/
|
|
137
|
+
function submitApprovalDecision(request_id, approver_id, decision, reason) {
|
|
138
|
+
try {
|
|
139
|
+
const request = (0, database_1.getApprovalRequest)(request_id);
|
|
140
|
+
if (!request) {
|
|
141
|
+
throw new Error(`Approval request ${request_id} not found`);
|
|
142
|
+
}
|
|
143
|
+
// Verify approver is authorized for this request
|
|
144
|
+
const approvers = JSON.parse(request.approver_ids);
|
|
145
|
+
if (!approvers.includes(approver_id)) {
|
|
146
|
+
throw new Error(`${approver_id} is not authorized to approve this request`);
|
|
147
|
+
}
|
|
148
|
+
// Check if already decided by this approver
|
|
149
|
+
const existingDecisions = (0, database_1.getApprovalDecisions)(request_id);
|
|
150
|
+
if (existingDecisions.some(d => d.approver_id === approver_id)) {
|
|
151
|
+
throw new Error(`${approver_id} has already made a decision on this request`);
|
|
152
|
+
}
|
|
153
|
+
// Record decision
|
|
154
|
+
const decision_id = `decision-${request_id}-${approver_id}-${Date.now()}`;
|
|
155
|
+
(0, database_1.submitApprovalDecision)(decision_id, request_id, approver_id, decision, reason);
|
|
156
|
+
// Return updated status
|
|
157
|
+
return getApprovalStatus(request_id);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw new Error(`Failed to submit decision: ${error.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Get the current approval status
|
|
165
|
+
*/
|
|
166
|
+
function getApprovalStatus(request_id) {
|
|
167
|
+
const request = (0, database_1.getApprovalRequest)(request_id);
|
|
168
|
+
if (!request) {
|
|
169
|
+
throw new Error(`Approval request ${request_id} not found`);
|
|
170
|
+
}
|
|
171
|
+
const decisions = (0, database_1.getApprovalDecisions)(request_id);
|
|
172
|
+
const approvers = JSON.parse(request.approver_ids);
|
|
173
|
+
const pendingApprovers = approvers.filter(a => !decisions.some(d => d.approver_id === a));
|
|
174
|
+
const approvedCount = decisions.filter(d => d.decision === 'approve').length;
|
|
175
|
+
const rejectedCount = decisions.filter(d => d.decision === 'reject').length;
|
|
176
|
+
// Determine if we can proceed
|
|
177
|
+
let can_proceed = false;
|
|
178
|
+
const approverIds = JSON.parse(request.approver_ids);
|
|
179
|
+
let status = 'pending';
|
|
180
|
+
if (rejectedCount > 0) {
|
|
181
|
+
status = 'rejected';
|
|
182
|
+
can_proceed = false;
|
|
183
|
+
}
|
|
184
|
+
else if (approvedCount >= approverIds.length) {
|
|
185
|
+
status = 'approved';
|
|
186
|
+
can_proceed = true;
|
|
187
|
+
}
|
|
188
|
+
else if (approvedCount > 0 && rejectedCount === 0) {
|
|
189
|
+
status = 'mixed';
|
|
190
|
+
can_proceed = false;
|
|
191
|
+
}
|
|
192
|
+
return {
|
|
193
|
+
request_id,
|
|
194
|
+
status,
|
|
195
|
+
approved_count: approvedCount,
|
|
196
|
+
rejected_count: rejectedCount,
|
|
197
|
+
pending_count: pendingApprovers.length,
|
|
198
|
+
required_approvers: approverIds.length,
|
|
199
|
+
decisions: decisions.map(d => ({
|
|
200
|
+
approver_id: d.approver_id,
|
|
201
|
+
decision: d.decision,
|
|
202
|
+
reason: d.reason ?? undefined,
|
|
203
|
+
})),
|
|
204
|
+
can_proceed,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if all required approvals are satisfied for a session rollback
|
|
209
|
+
*/
|
|
210
|
+
function canProceedWithRollback(request_id) {
|
|
211
|
+
try {
|
|
212
|
+
const status = getApprovalStatus(request_id);
|
|
213
|
+
return status.can_proceed;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get all pending approvals for a specific approver
|
|
221
|
+
*/
|
|
222
|
+
function getPendingApprovalsForUser(approver_id) {
|
|
223
|
+
const allRequests = (0, database_1.getSessionApprovalRequests)(''); // Get all (filter by approver below)
|
|
224
|
+
// Filter: only pending requests where user is an approver and hasn't decided
|
|
225
|
+
return allRequests.filter(req => {
|
|
226
|
+
if (req.status !== 'pending')
|
|
227
|
+
return false;
|
|
228
|
+
const approvers = JSON.parse(req.approver_ids);
|
|
229
|
+
if (!approvers.includes(approver_id))
|
|
230
|
+
return false;
|
|
231
|
+
const decisions = (0, database_1.getApprovalDecisions)(req.request_id);
|
|
232
|
+
return !decisions.some(d => d.approver_id === approver_id);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Get approval history for a session
|
|
237
|
+
*/
|
|
238
|
+
function getApprovalHistory(session_id) {
|
|
239
|
+
return (0, database_1.getSessionApprovalRequests)(session_id);
|
|
240
|
+
}
|
|
241
|
+
exports.default = {
|
|
242
|
+
determineRequiredApprovers,
|
|
243
|
+
createApprovalRequestForSession,
|
|
244
|
+
submitApprovalDecision,
|
|
245
|
+
getApprovalStatus,
|
|
246
|
+
canProceedWithRollback,
|
|
247
|
+
getPendingApprovalsForUser,
|
|
248
|
+
getApprovalHistory,
|
|
249
|
+
};
|