@way_marks/server 0.8.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
package/dist/api/server.js
CHANGED
|
@@ -41,8 +41,11 @@ const express_1 = __importDefault(require("express"));
|
|
|
41
41
|
const fs = __importStar(require("fs"));
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const database_1 = require("../db/database");
|
|
44
|
+
const manager_1 = require("../rollback/manager");
|
|
44
45
|
const engine_1 = require("../policies/engine");
|
|
45
46
|
const handler_1 = require("../approvals/handler");
|
|
47
|
+
const manager_2 = require("../approval/manager");
|
|
48
|
+
const manager_3 = require("../escalation/manager");
|
|
46
49
|
// Import registry for Phase 2 hub navigation
|
|
47
50
|
const registryPath = path.join(process.env.HOME || process.env.USERPROFILE || '', '.waymark', 'registry.json');
|
|
48
51
|
function getRegistryProjects() {
|
|
@@ -317,6 +320,104 @@ app.get('/api/sessions', (req, res) => {
|
|
|
317
320
|
res.status(500).json({ error: err.message });
|
|
318
321
|
}
|
|
319
322
|
});
|
|
323
|
+
// Phase 1: GET /api/sessions/:session_id — get session details
|
|
324
|
+
app.get('/api/sessions/:session_id', (req, res) => {
|
|
325
|
+
try {
|
|
326
|
+
const { session_id } = req.params;
|
|
327
|
+
const session = (0, database_1.getSession)(session_id);
|
|
328
|
+
if (!session) {
|
|
329
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
330
|
+
}
|
|
331
|
+
res.json(session);
|
|
332
|
+
}
|
|
333
|
+
catch (err) {
|
|
334
|
+
res.status(500).json({ error: err.message });
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
// Phase 1: GET /api/sessions/:session_id/actions — get all actions in session
|
|
338
|
+
app.get('/api/sessions/:session_id/actions', (req, res) => {
|
|
339
|
+
try {
|
|
340
|
+
const { session_id } = req.params;
|
|
341
|
+
const actions = (0, database_1.getSessionActions)(session_id);
|
|
342
|
+
res.json({
|
|
343
|
+
session_id,
|
|
344
|
+
action_count: actions.length,
|
|
345
|
+
actions,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
catch (err) {
|
|
349
|
+
res.status(500).json({ error: err.message });
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
// Phase 1: POST /api/sessions/:session_id/rollback — rollback entire session
|
|
353
|
+
app.post('/api/sessions/:session_id/rollback', (req, res) => {
|
|
354
|
+
try {
|
|
355
|
+
const { session_id } = req.params;
|
|
356
|
+
// Get session
|
|
357
|
+
const session = (0, database_1.getSession)(session_id);
|
|
358
|
+
if (!session) {
|
|
359
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
360
|
+
}
|
|
361
|
+
// Prevent rolling back already rolled back sessions
|
|
362
|
+
if (session.status === 'rolled_back') {
|
|
363
|
+
return res.status(400).json({ error: 'Session already rolled back' });
|
|
364
|
+
}
|
|
365
|
+
// Get all actions in session
|
|
366
|
+
const actions = (0, database_1.getSessionActions)(session_id);
|
|
367
|
+
if (actions.length === 0) {
|
|
368
|
+
return res.status(400).json({ error: 'Session has no actions to rollback' });
|
|
369
|
+
}
|
|
370
|
+
// Validate all actions are reversible
|
|
371
|
+
const validation = (0, manager_1.validateRollbackable)(actions);
|
|
372
|
+
if (!validation.isValid) {
|
|
373
|
+
return res.status(400).json({
|
|
374
|
+
error: 'Cannot rollback session',
|
|
375
|
+
validation,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Create and execute rollback transaction
|
|
379
|
+
const transaction = (0, manager_1.createRollbackTransaction)(session_id, actions);
|
|
380
|
+
const result = (0, manager_1.executeRollbackTransaction)(transaction);
|
|
381
|
+
if (!result.success) {
|
|
382
|
+
return res.status(500).json({
|
|
383
|
+
error: result.error,
|
|
384
|
+
message: 'Rollback failed',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
res.json({
|
|
388
|
+
success: true,
|
|
389
|
+
message: `Rolled back session ${session_id}`,
|
|
390
|
+
actions_rolled_back: actions.length,
|
|
391
|
+
files_restored: result.filesRestored,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
catch (err) {
|
|
395
|
+
res.status(500).json({ error: err.message });
|
|
396
|
+
}
|
|
397
|
+
});
|
|
398
|
+
// Phase 1: GET /api/sessions/:session_id/status — check rollback status
|
|
399
|
+
app.get('/api/sessions/:session_id/status', (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const { session_id } = req.params;
|
|
402
|
+
const session = (0, database_1.getSession)(session_id);
|
|
403
|
+
if (!session) {
|
|
404
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
405
|
+
}
|
|
406
|
+
const actions = (0, database_1.getSessionActions)(session_id);
|
|
407
|
+
const rolledBackCount = actions.filter((a) => a.rolled_back === 1).length;
|
|
408
|
+
res.json({
|
|
409
|
+
session_id,
|
|
410
|
+
status: session.status,
|
|
411
|
+
total_actions: actions.length,
|
|
412
|
+
rolled_back_actions: rolledBackCount,
|
|
413
|
+
rolled_back_at: session.rolled_back_at,
|
|
414
|
+
is_rolled_back: session.status === 'rolled_back',
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
catch (err) {
|
|
418
|
+
res.status(500).json({ error: err.message });
|
|
419
|
+
}
|
|
420
|
+
});
|
|
320
421
|
// GET /api/config
|
|
321
422
|
app.get('/api/config', (req, res) => {
|
|
322
423
|
try {
|
|
@@ -360,6 +461,503 @@ app.post('/api/registry/cleanup', (req, res) => {
|
|
|
360
461
|
res.status(500).json({ error: err.message });
|
|
361
462
|
}
|
|
362
463
|
});
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// PHASE 2: Team Approval Routing Endpoints
|
|
466
|
+
// ============================================================================
|
|
467
|
+
// GET /api/team/members — list all team members
|
|
468
|
+
app.get('/api/team/members', (req, res) => {
|
|
469
|
+
try {
|
|
470
|
+
const members = (0, database_1.getAllTeamMembers)();
|
|
471
|
+
res.json(members);
|
|
472
|
+
}
|
|
473
|
+
catch (err) {
|
|
474
|
+
res.status(500).json({ error: err.message });
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
// POST /api/team/members — add team member
|
|
478
|
+
app.post('/api/team/members', (req, res) => {
|
|
479
|
+
try {
|
|
480
|
+
const { member_id, name, email, slack_id } = req.body;
|
|
481
|
+
const added_by = req.body.added_by || 'system';
|
|
482
|
+
if (!member_id || !name || !email) {
|
|
483
|
+
return res.status(400).json({ error: 'Missing required fields: member_id, name, email' });
|
|
484
|
+
}
|
|
485
|
+
// Check if email already exists
|
|
486
|
+
const existing = (0, database_1.getAllTeamMembers)().find(m => m.email === email);
|
|
487
|
+
if (existing) {
|
|
488
|
+
return res.status(400).json({ error: `Email ${email} already in use` });
|
|
489
|
+
}
|
|
490
|
+
(0, database_1.addTeamMember)(member_id, name, email, added_by, slack_id);
|
|
491
|
+
res.json({ success: true, member_id, message: `Added team member: ${name}` });
|
|
492
|
+
}
|
|
493
|
+
catch (err) {
|
|
494
|
+
res.status(500).json({ error: err.message });
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
// DELETE /api/team/members/:member_id — remove team member
|
|
498
|
+
app.delete('/api/team/members/:member_id', (req, res) => {
|
|
499
|
+
try {
|
|
500
|
+
const { member_id } = req.params;
|
|
501
|
+
const member = (0, database_1.getTeamMember)(member_id);
|
|
502
|
+
if (!member) {
|
|
503
|
+
return res.status(404).json({ error: `Team member ${member_id} not found` });
|
|
504
|
+
}
|
|
505
|
+
(0, database_1.removeTeamMember)(member_id);
|
|
506
|
+
res.json({ success: true, message: `Removed team member: ${member.name}` });
|
|
507
|
+
}
|
|
508
|
+
catch (err) {
|
|
509
|
+
res.status(500).json({ error: err.message });
|
|
510
|
+
}
|
|
511
|
+
});
|
|
512
|
+
// GET /api/approval-routes — list all approval routing rules
|
|
513
|
+
app.get('/api/approval-routes', (req, res) => {
|
|
514
|
+
try {
|
|
515
|
+
const routes = (0, database_1.getAllApprovalRoutes)();
|
|
516
|
+
res.json(routes);
|
|
517
|
+
}
|
|
518
|
+
catch (err) {
|
|
519
|
+
res.status(500).json({ error: err.message });
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
// POST /api/approval-routes — create approval routing rule
|
|
523
|
+
app.post('/api/approval-routes', (req, res) => {
|
|
524
|
+
try {
|
|
525
|
+
const { route_id, name, approver_ids, description, condition_type, condition_json } = req.body;
|
|
526
|
+
const created_by = req.body.created_by || 'system';
|
|
527
|
+
if (!route_id || !name || !Array.isArray(approver_ids) || approver_ids.length === 0) {
|
|
528
|
+
return res.status(400).json({ error: 'Missing required fields: route_id, name, approver_ids (array)' });
|
|
529
|
+
}
|
|
530
|
+
(0, database_1.addApprovalRoute)(route_id, name, approver_ids, created_by, description, condition_type, condition_json);
|
|
531
|
+
res.json({ success: true, route_id, message: `Created approval route: ${name}` });
|
|
532
|
+
}
|
|
533
|
+
catch (err) {
|
|
534
|
+
res.status(500).json({ error: err.message });
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
// PUT /api/approval-routes/:route_id — update approval routing rule
|
|
538
|
+
app.put('/api/approval-routes/:route_id', (req, res) => {
|
|
539
|
+
try {
|
|
540
|
+
const { route_id } = req.params;
|
|
541
|
+
const { name, description, approver_ids } = req.body;
|
|
542
|
+
const route = (0, database_1.getApprovalRoute)(route_id);
|
|
543
|
+
if (!route) {
|
|
544
|
+
return res.status(404).json({ error: `Approval route ${route_id} not found` });
|
|
545
|
+
}
|
|
546
|
+
(0, database_1.updateApprovalRoute)(route_id, { name, description, approver_ids });
|
|
547
|
+
res.json({ success: true, message: `Updated approval route: ${route_id}` });
|
|
548
|
+
}
|
|
549
|
+
catch (err) {
|
|
550
|
+
res.status(500).json({ error: err.message });
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
// DELETE /api/approval-routes/:route_id — delete approval routing rule
|
|
554
|
+
app.delete('/api/approval-routes/:route_id', (req, res) => {
|
|
555
|
+
try {
|
|
556
|
+
const { route_id } = req.params;
|
|
557
|
+
const route = (0, database_1.getApprovalRoute)(route_id);
|
|
558
|
+
if (!route) {
|
|
559
|
+
return res.status(404).json({ error: `Approval route ${route_id} not found` });
|
|
560
|
+
}
|
|
561
|
+
(0, database_1.deleteApprovalRoute)(route_id);
|
|
562
|
+
res.json({ success: true, message: `Deleted approval route: ${route_id}` });
|
|
563
|
+
}
|
|
564
|
+
catch (err) {
|
|
565
|
+
res.status(500).json({ error: err.message });
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
// GET /api/approvals/pending — get pending approvals for current user
|
|
569
|
+
app.get('/api/approvals/pending', (req, res) => {
|
|
570
|
+
try {
|
|
571
|
+
const approver_id = req.query.approver_id;
|
|
572
|
+
const pending = (0, database_1.getPendingApprovals)(approver_id);
|
|
573
|
+
res.json(pending);
|
|
574
|
+
}
|
|
575
|
+
catch (err) {
|
|
576
|
+
res.status(500).json({ error: err.message });
|
|
577
|
+
}
|
|
578
|
+
});
|
|
579
|
+
// GET /api/approvals/:request_id — get approval request details and status
|
|
580
|
+
app.get('/api/approvals/:request_id', (req, res) => {
|
|
581
|
+
try {
|
|
582
|
+
const { request_id } = req.params;
|
|
583
|
+
const request = (0, database_1.getApprovalRequest)(request_id);
|
|
584
|
+
if (!request) {
|
|
585
|
+
return res.status(404).json({ error: `Approval request ${request_id} not found` });
|
|
586
|
+
}
|
|
587
|
+
try {
|
|
588
|
+
const status = (0, manager_2.getApprovalStatus)(request_id);
|
|
589
|
+
res.json({
|
|
590
|
+
request,
|
|
591
|
+
status,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
catch (err) {
|
|
595
|
+
res.json({ request, status_error: err.message });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
res.status(500).json({ error: err.message });
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
// POST /api/approvals/:request_id/approve — submit approval decision
|
|
603
|
+
app.post('/api/approvals/:request_id/approve', (req, res) => {
|
|
604
|
+
try {
|
|
605
|
+
const { request_id } = req.params;
|
|
606
|
+
const { approver_id, reason } = req.body;
|
|
607
|
+
if (!approver_id) {
|
|
608
|
+
return res.status(400).json({ error: 'Missing required field: approver_id' });
|
|
609
|
+
}
|
|
610
|
+
const status = (0, manager_2.submitApprovalDecision)(request_id, approver_id, 'approve', reason);
|
|
611
|
+
res.json({
|
|
612
|
+
success: true,
|
|
613
|
+
message: `Approved by ${approver_id}`,
|
|
614
|
+
status,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
res.status(400).json({ error: err.message });
|
|
619
|
+
}
|
|
620
|
+
});
|
|
621
|
+
// POST /api/approvals/:request_id/reject — submit rejection decision
|
|
622
|
+
app.post('/api/approvals/:request_id/reject', (req, res) => {
|
|
623
|
+
try {
|
|
624
|
+
const { request_id } = req.params;
|
|
625
|
+
const { approver_id, reason } = req.body;
|
|
626
|
+
if (!approver_id) {
|
|
627
|
+
return res.status(400).json({ error: 'Missing required field: approver_id' });
|
|
628
|
+
}
|
|
629
|
+
const status = (0, manager_2.submitApprovalDecision)(request_id, approver_id, 'reject', reason);
|
|
630
|
+
res.json({
|
|
631
|
+
success: true,
|
|
632
|
+
message: `Rejected by ${approver_id}`,
|
|
633
|
+
status,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
catch (err) {
|
|
637
|
+
res.status(400).json({ error: err.message });
|
|
638
|
+
}
|
|
639
|
+
});
|
|
640
|
+
// GET /api/approvals/history/:session_id — get approval history for session
|
|
641
|
+
app.get('/api/approvals/history/:session_id', (req, res) => {
|
|
642
|
+
try {
|
|
643
|
+
const { session_id } = req.params;
|
|
644
|
+
const requests = (0, database_1.getSessionApprovalRequests)(session_id);
|
|
645
|
+
res.json({
|
|
646
|
+
session_id,
|
|
647
|
+
approval_requests: requests,
|
|
648
|
+
total: requests.length,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
res.status(500).json({ error: err.message });
|
|
653
|
+
}
|
|
654
|
+
});
|
|
655
|
+
// ============================================================================
|
|
656
|
+
// PHASE 3: Approval Escalation Endpoints
|
|
657
|
+
// ============================================================================
|
|
658
|
+
// GET /api/escalations/rules — list all escalation rules
|
|
659
|
+
app.get('/api/escalations/rules', (req, res) => {
|
|
660
|
+
try {
|
|
661
|
+
const rules = (0, database_1.getAllEscalationRules)();
|
|
662
|
+
res.json(rules);
|
|
663
|
+
}
|
|
664
|
+
catch (err) {
|
|
665
|
+
res.status(500).json({ error: err.message });
|
|
666
|
+
}
|
|
667
|
+
});
|
|
668
|
+
// POST /api/escalations/rules — create escalation rule
|
|
669
|
+
app.post('/api/escalations/rules', (req, res) => {
|
|
670
|
+
try {
|
|
671
|
+
const { rule_id, name, escalation_targets, description, timeout_hours } = req.body;
|
|
672
|
+
const created_by = req.body.created_by || 'system';
|
|
673
|
+
if (!rule_id || !name || !Array.isArray(escalation_targets) || escalation_targets.length === 0) {
|
|
674
|
+
return res.status(400).json({ error: 'Missing required fields: rule_id, name, escalation_targets (array)' });
|
|
675
|
+
}
|
|
676
|
+
(0, database_1.addEscalationRule)(rule_id, name, escalation_targets, created_by, description, timeout_hours);
|
|
677
|
+
res.json({ success: true, rule_id, message: `Created escalation rule: ${name}` });
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
res.status(500).json({ error: err.message });
|
|
681
|
+
}
|
|
682
|
+
});
|
|
683
|
+
// PUT /api/escalations/rules/:rule_id — update escalation rule
|
|
684
|
+
app.put('/api/escalations/rules/:rule_id', (req, res) => {
|
|
685
|
+
try {
|
|
686
|
+
const { rule_id } = req.params;
|
|
687
|
+
const { name, description, escalation_targets, timeout_hours } = req.body;
|
|
688
|
+
const rule = (0, database_1.getEscalationRule)(rule_id);
|
|
689
|
+
if (!rule) {
|
|
690
|
+
return res.status(404).json({ error: `Escalation rule ${rule_id} not found` });
|
|
691
|
+
}
|
|
692
|
+
(0, database_1.updateEscalationRule)(rule_id, { name, description, escalation_targets, timeout_hours });
|
|
693
|
+
res.json({ success: true, message: `Updated escalation rule: ${rule_id}` });
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
res.status(500).json({ error: err.message });
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
// DELETE /api/escalations/rules/:rule_id — delete escalation rule
|
|
700
|
+
app.delete('/api/escalations/rules/:rule_id', (req, res) => {
|
|
701
|
+
try {
|
|
702
|
+
const { rule_id } = req.params;
|
|
703
|
+
const rule = (0, database_1.getEscalationRule)(rule_id);
|
|
704
|
+
if (!rule) {
|
|
705
|
+
return res.status(404).json({ error: `Escalation rule ${rule_id} not found` });
|
|
706
|
+
}
|
|
707
|
+
(0, database_1.deleteEscalationRule)(rule_id);
|
|
708
|
+
res.json({ success: true, message: `Deleted escalation rule: ${rule_id}` });
|
|
709
|
+
}
|
|
710
|
+
catch (err) {
|
|
711
|
+
res.status(500).json({ error: err.message });
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
// GET /api/escalations/pending — get pending escalations
|
|
715
|
+
app.get('/api/escalations/pending', (req, res) => {
|
|
716
|
+
try {
|
|
717
|
+
const pending = (0, database_1.getPendingEscalations)();
|
|
718
|
+
res.json(pending);
|
|
719
|
+
}
|
|
720
|
+
catch (err) {
|
|
721
|
+
res.status(500).json({ error: err.message });
|
|
722
|
+
}
|
|
723
|
+
});
|
|
724
|
+
// GET /api/escalations/:request_id — get escalation details and status
|
|
725
|
+
app.get('/api/escalations/:request_id', (req, res) => {
|
|
726
|
+
try {
|
|
727
|
+
const { request_id } = req.params;
|
|
728
|
+
const request = (0, database_1.getEscalationRequest)(request_id);
|
|
729
|
+
if (!request) {
|
|
730
|
+
return res.status(404).json({ error: `Escalation request ${request_id} not found` });
|
|
731
|
+
}
|
|
732
|
+
try {
|
|
733
|
+
const status = (0, manager_3.getEscalationStatus)(request_id);
|
|
734
|
+
res.json({
|
|
735
|
+
request,
|
|
736
|
+
status,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
res.json({ request, status_error: err.message });
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
catch (err) {
|
|
744
|
+
res.status(500).json({ error: err.message });
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
// POST /api/escalations/:request_id/decide — submit escalation decision
|
|
748
|
+
app.post('/api/escalations/:request_id/decide', (req, res) => {
|
|
749
|
+
try {
|
|
750
|
+
const { request_id } = req.params;
|
|
751
|
+
const { target_id, decision, reason } = req.body;
|
|
752
|
+
if (!target_id || !decision) {
|
|
753
|
+
return res.status(400).json({ error: 'Missing required fields: target_id, decision' });
|
|
754
|
+
}
|
|
755
|
+
if (!['proceed', 'block'].includes(decision)) {
|
|
756
|
+
return res.status(400).json({ error: 'Decision must be "proceed" or "block"' });
|
|
757
|
+
}
|
|
758
|
+
const status = (0, manager_3.submitEscalationDecision)(request_id, target_id, decision, reason);
|
|
759
|
+
res.json({
|
|
760
|
+
success: true,
|
|
761
|
+
message: `${target_id} decided to ${decision}`,
|
|
762
|
+
status,
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
catch (err) {
|
|
766
|
+
res.status(400).json({ error: err.message });
|
|
767
|
+
}
|
|
768
|
+
});
|
|
769
|
+
// GET /api/escalations/history/:session_id — get escalation history for session
|
|
770
|
+
app.get('/api/escalations/history/:session_id', (req, res) => {
|
|
771
|
+
try {
|
|
772
|
+
const { session_id } = req.params;
|
|
773
|
+
const history = (0, manager_3.getEscalationHistoryForSession)(session_id);
|
|
774
|
+
res.json({
|
|
775
|
+
session_id,
|
|
776
|
+
escalation_requests: history,
|
|
777
|
+
total: history.length,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
catch (err) {
|
|
781
|
+
res.status(500).json({ error: err.message });
|
|
782
|
+
}
|
|
783
|
+
});
|
|
784
|
+
// ============================================================================
|
|
785
|
+
// Phase 4: Remediation Endpoints
|
|
786
|
+
// ============================================================================
|
|
787
|
+
// POST /api/remediation/assess — analyze risk of session
|
|
788
|
+
app.post('/api/remediation/assess', (req, res) => {
|
|
789
|
+
try {
|
|
790
|
+
const { session_id } = req.body;
|
|
791
|
+
if (!session_id) {
|
|
792
|
+
return res.status(400).json({ error: 'Missing required field: session_id' });
|
|
793
|
+
}
|
|
794
|
+
// Get session and actions
|
|
795
|
+
const session = (0, database_1.getSession)(session_id);
|
|
796
|
+
if (!session) {
|
|
797
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
798
|
+
}
|
|
799
|
+
const actions = (0, database_1.getSessionActions)(session_id);
|
|
800
|
+
// Note: In production, would import and use actual Risk Assessment Engine
|
|
801
|
+
// For now, return placeholder
|
|
802
|
+
res.json({
|
|
803
|
+
session_id,
|
|
804
|
+
action_count: actions.length,
|
|
805
|
+
risk_assessment: {
|
|
806
|
+
score: 5.0,
|
|
807
|
+
level: 'medium',
|
|
808
|
+
message: 'Risk assessment module available in Phase 4A implementation',
|
|
809
|
+
},
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
catch (err) {
|
|
813
|
+
res.status(500).json({ error: err.message });
|
|
814
|
+
}
|
|
815
|
+
});
|
|
816
|
+
// POST /api/remediation/evaluate-policy — check policy compliance
|
|
817
|
+
app.post('/api/remediation/evaluate-policy', (req, res) => {
|
|
818
|
+
try {
|
|
819
|
+
const { session_id } = req.body;
|
|
820
|
+
if (!session_id) {
|
|
821
|
+
return res.status(400).json({ error: 'Missing required field: session_id' });
|
|
822
|
+
}
|
|
823
|
+
// Get session and actions
|
|
824
|
+
const session = (0, database_1.getSession)(session_id);
|
|
825
|
+
if (!session) {
|
|
826
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
827
|
+
}
|
|
828
|
+
res.json({
|
|
829
|
+
session_id,
|
|
830
|
+
violations: [],
|
|
831
|
+
message: 'Policy evaluation module available in Phase 4B implementation',
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
res.status(500).json({ error: err.message });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
// POST /api/remediation/recommend — get remediation strategies
|
|
839
|
+
app.post('/api/remediation/recommend', (req, res) => {
|
|
840
|
+
try {
|
|
841
|
+
const { session_id } = req.body;
|
|
842
|
+
if (!session_id) {
|
|
843
|
+
return res.status(400).json({ error: 'Missing required field: session_id' });
|
|
844
|
+
}
|
|
845
|
+
// Get session and actions
|
|
846
|
+
const session = (0, database_1.getSession)(session_id);
|
|
847
|
+
if (!session) {
|
|
848
|
+
return res.status(404).json({ error: `Session ${session_id} not found` });
|
|
849
|
+
}
|
|
850
|
+
res.json({
|
|
851
|
+
session_id,
|
|
852
|
+
recommendations: {
|
|
853
|
+
primary_strategy: 'escalation',
|
|
854
|
+
alternatives: [],
|
|
855
|
+
message: 'Remediation recommender available in Phase 4C implementation',
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
catch (err) {
|
|
860
|
+
res.status(500).json({ error: err.message });
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
// GET /api/remediation/blocks — list blocked sessions
|
|
864
|
+
app.get('/api/remediation/blocks', (req, res) => {
|
|
865
|
+
try {
|
|
866
|
+
res.json({
|
|
867
|
+
blocks: [],
|
|
868
|
+
total: 0,
|
|
869
|
+
message: 'Auto-block storage available in Phase 4D implementation',
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
catch (err) {
|
|
873
|
+
res.status(500).json({ error: err.message });
|
|
874
|
+
}
|
|
875
|
+
});
|
|
876
|
+
// POST /api/remediation/blocks/:block_id/unblock — admin override
|
|
877
|
+
app.post('/api/remediation/blocks/:block_id/unblock', (req, res) => {
|
|
878
|
+
try {
|
|
879
|
+
const { block_id } = req.params;
|
|
880
|
+
const { reason } = req.body;
|
|
881
|
+
// Check admin role (placeholder)
|
|
882
|
+
const isAdmin = req.headers['x-user-role'] === 'admin';
|
|
883
|
+
if (!isAdmin) {
|
|
884
|
+
return res.status(401).json({ error: 'Admin role required to override blocks' });
|
|
885
|
+
}
|
|
886
|
+
res.json({
|
|
887
|
+
block_id,
|
|
888
|
+
unblocked: true,
|
|
889
|
+
unblock_reason: reason,
|
|
890
|
+
message: 'Block override recorded',
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
catch (err) {
|
|
894
|
+
res.status(500).json({ error: err.message });
|
|
895
|
+
}
|
|
896
|
+
});
|
|
897
|
+
// GET /api/remediation/policies — list active policies
|
|
898
|
+
app.get('/api/remediation/policies', (req, res) => {
|
|
899
|
+
try {
|
|
900
|
+
res.json({
|
|
901
|
+
policies: [],
|
|
902
|
+
total: 0,
|
|
903
|
+
message: 'Policy storage available in Phase 4B implementation',
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
catch (err) {
|
|
907
|
+
res.status(500).json({ error: err.message });
|
|
908
|
+
}
|
|
909
|
+
});
|
|
910
|
+
// POST /api/remediation/policies — create policy
|
|
911
|
+
app.post('/api/remediation/policies', (req, res) => {
|
|
912
|
+
try {
|
|
913
|
+
const { name, description, category, rules } = req.body;
|
|
914
|
+
if (!name || !category) {
|
|
915
|
+
return res.status(400).json({ error: 'Missing required fields: name, category' });
|
|
916
|
+
}
|
|
917
|
+
res.status(201).json({
|
|
918
|
+
policy_id: `policy-${Date.now()}`,
|
|
919
|
+
name,
|
|
920
|
+
description,
|
|
921
|
+
category,
|
|
922
|
+
rules: rules || [],
|
|
923
|
+
enabled: true,
|
|
924
|
+
created_at: new Date().toISOString(),
|
|
925
|
+
message: 'Policy creation implemented in Phase 4B',
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
catch (err) {
|
|
929
|
+
res.status(500).json({ error: err.message });
|
|
930
|
+
}
|
|
931
|
+
});
|
|
932
|
+
// PUT /api/remediation/policies/:policy_id — update policy
|
|
933
|
+
app.put('/api/remediation/policies/:policy_id', (req, res) => {
|
|
934
|
+
try {
|
|
935
|
+
const { policy_id } = req.params;
|
|
936
|
+
const { name, description, enabled, rules } = req.body;
|
|
937
|
+
res.json({
|
|
938
|
+
policy_id,
|
|
939
|
+
name,
|
|
940
|
+
description,
|
|
941
|
+
enabled,
|
|
942
|
+
rules,
|
|
943
|
+
updated_at: new Date().toISOString(),
|
|
944
|
+
message: 'Policy update implemented in Phase 4B',
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
res.status(500).json({ error: err.message });
|
|
949
|
+
}
|
|
950
|
+
});
|
|
951
|
+
// DELETE /api/remediation/policies/:policy_id — delete policy
|
|
952
|
+
app.delete('/api/remediation/policies/:policy_id', (req, res) => {
|
|
953
|
+
try {
|
|
954
|
+
const { policy_id } = req.params;
|
|
955
|
+
res.status(204).send();
|
|
956
|
+
}
|
|
957
|
+
catch (err) {
|
|
958
|
+
res.status(500).json({ error: err.message });
|
|
959
|
+
}
|
|
960
|
+
});
|
|
363
961
|
// Fallback: serve UI for any unmatched route
|
|
364
962
|
app.get('*', (req, res) => {
|
|
365
963
|
res.sendFile(path.join(UI_DIR, 'index.html'));
|