@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.
@@ -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'));