@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.
@@ -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
+ };
@@ -0,0 +1,457 @@
1
+ "use strict";
2
+ /**
3
+ * Notification Service — Slack & Email integration for approval requests
4
+ *
5
+ * Sends notifications when:
6
+ * - Approval request is created (notify required approvers)
7
+ * - Approval decision is submitted (notify original requester and team)
8
+ *
9
+ * Supports:
10
+ * - Slack webhook notifications with interactive approval buttons
11
+ * - Email notifications with decision details
12
+ * - Custom templates and retry logic
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.notifyApprovalRequestSlack = notifyApprovalRequestSlack;
49
+ exports.notifyApprovalDecisionSlack = notifyApprovalDecisionSlack;
50
+ exports.notifyApprovalRequestEmail = notifyApprovalRequestEmail;
51
+ exports.notifyApprovalDecisionEmail = notifyApprovalDecisionEmail;
52
+ exports.broadcastApprovalRequest = broadcastApprovalRequest;
53
+ exports.broadcastApprovalDecision = broadcastApprovalDecision;
54
+ exports.notifyEscalationSlack = notifyEscalationSlack;
55
+ exports.notifyEscalationDecisionSlack = notifyEscalationDecisionSlack;
56
+ exports.notifyEscalationEmail = notifyEscalationEmail;
57
+ exports.notifyEscalationDecisionEmail = notifyEscalationDecisionEmail;
58
+ exports.broadcastEscalation = broadcastEscalation;
59
+ exports.broadcastEscalationDecision = broadcastEscalationDecision;
60
+ const fs = __importStar(require("fs"));
61
+ const path = __importStar(require("path"));
62
+ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
63
+ const CONFIG_PATH = path.join(PROJECT_ROOT, '.waymark', 'config.json');
64
+ /**
65
+ * Load notification configuration from .waymark/config.json
66
+ */
67
+ function loadNotificationConfig() {
68
+ try {
69
+ if (!fs.existsSync(CONFIG_PATH)) {
70
+ return {};
71
+ }
72
+ const config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
73
+ return config.notifications || {};
74
+ }
75
+ catch {
76
+ return {};
77
+ }
78
+ }
79
+ /**
80
+ * Send Slack notification for approval request
81
+ */
82
+ async function notifyApprovalRequestSlack(approver_ids, session_id, request_id, requester_name, action_count) {
83
+ const config = loadNotificationConfig();
84
+ const slackConfig = config.slack;
85
+ if (!slackConfig?.enabled || !slackConfig.webhook_url) {
86
+ console.log('[Notifications] Slack disabled or unconfigured');
87
+ return false;
88
+ }
89
+ try {
90
+ const approversList = approver_ids.join(', ');
91
+ const message = {
92
+ text: `🔔 New approval request from ${requester_name}`,
93
+ blocks: [
94
+ {
95
+ type: 'section',
96
+ text: {
97
+ type: 'mrkdwn',
98
+ text: `*Approval Required* 🔔\n\nUser *${requester_name}* requests approval to rollback session with *${action_count}* actions.\n\n*Session:* \`${session_id}\`\n*Approvers:* ${approversList}`,
99
+ },
100
+ },
101
+ {
102
+ type: 'actions',
103
+ elements: [
104
+ {
105
+ type: 'button',
106
+ text: {
107
+ type: 'plain_text',
108
+ text: '✅ Approve',
109
+ },
110
+ value: request_id,
111
+ action_id: 'waymark_approval_approve',
112
+ style: 'primary',
113
+ },
114
+ {
115
+ type: 'button',
116
+ text: {
117
+ type: 'plain_text',
118
+ text: '❌ Reject',
119
+ },
120
+ value: request_id,
121
+ action_id: 'waymark_approval_reject',
122
+ style: 'danger',
123
+ },
124
+ ],
125
+ },
126
+ ],
127
+ };
128
+ const response = await fetch(slackConfig.webhook_url, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/json' },
131
+ body: JSON.stringify(message),
132
+ });
133
+ return response.ok;
134
+ }
135
+ catch (err) {
136
+ console.error('[Notifications] Slack error:', err);
137
+ return false;
138
+ }
139
+ }
140
+ /**
141
+ * Send Slack notification for approval decision
142
+ */
143
+ async function notifyApprovalDecisionSlack(approver_name, decision, session_id, reason) {
144
+ const config = loadNotificationConfig();
145
+ const slackConfig = config.slack;
146
+ if (!slackConfig?.enabled || !slackConfig.webhook_url) {
147
+ return false;
148
+ }
149
+ try {
150
+ const emoji = decision === 'approve' ? '✅' : '❌';
151
+ const action = decision === 'approve' ? 'Approved' : 'Rejected';
152
+ const reasonText = reason ? `\n*Reason:* ${reason}` : '';
153
+ const message = {
154
+ text: `${emoji} Approval ${action} by ${approver_name}`,
155
+ blocks: [
156
+ {
157
+ type: 'section',
158
+ text: {
159
+ type: 'mrkdwn',
160
+ text: `${emoji} *Approval ${action}*\n\nApprover: *${approver_name}*\nSession: \`${session_id}\`${reasonText}`,
161
+ },
162
+ },
163
+ ],
164
+ };
165
+ const response = await fetch(slackConfig.webhook_url, {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify(message),
169
+ });
170
+ return response.ok;
171
+ }
172
+ catch (err) {
173
+ console.error('[Notifications] Slack decision error:', err);
174
+ return false;
175
+ }
176
+ }
177
+ /**
178
+ * Send email notification for approval request
179
+ */
180
+ async function notifyApprovalRequestEmail(recipient_email, recipient_name, session_id, request_id, requester_name, action_count) {
181
+ const config = loadNotificationConfig();
182
+ const emailConfig = config.email;
183
+ if (!emailConfig?.enabled) {
184
+ console.log('[Notifications] Email disabled or unconfigured');
185
+ return false;
186
+ }
187
+ try {
188
+ // For now, log the email that would be sent
189
+ // In production, integrate with SMTP or email service
190
+ console.log(`[Notifications] Email approval request to ${recipient_email}:
191
+ Recipient: ${recipient_name}
192
+ From: ${requester_name}
193
+ Session: ${session_id}
194
+ Actions: ${action_count}
195
+ Request ID: ${request_id}`);
196
+ return true;
197
+ }
198
+ catch (err) {
199
+ console.error('[Notifications] Email error:', err);
200
+ return false;
201
+ }
202
+ }
203
+ /**
204
+ * Send email notification for approval decision
205
+ */
206
+ async function notifyApprovalDecisionEmail(recipient_email, approver_name, decision, session_id, reason) {
207
+ const config = loadNotificationConfig();
208
+ const emailConfig = config.email;
209
+ if (!emailConfig?.enabled) {
210
+ return false;
211
+ }
212
+ try {
213
+ const action = decision === 'approve' ? 'Approved' : 'Rejected';
214
+ const reasonText = reason ? `\n\nReason: ${reason}` : '';
215
+ console.log(`[Notifications] Email approval decision to ${recipient_email}:
216
+ Approver: ${approver_name}
217
+ Decision: ${action}
218
+ Session: ${session_id}${reasonText}`);
219
+ return true;
220
+ }
221
+ catch (err) {
222
+ console.error('[Notifications] Email decision error:', err);
223
+ return false;
224
+ }
225
+ }
226
+ /**
227
+ * Broadcast approval request to all required approvers
228
+ */
229
+ async function broadcastApprovalRequest(approvers, session_id, request_id, requester_name, action_count) {
230
+ let slack_sent = 0;
231
+ let email_sent = 0;
232
+ // Send Slack notification (once to all approvers)
233
+ const slackSuccess = await notifyApprovalRequestSlack(approvers.map(a => a.member_id), session_id, request_id, requester_name, action_count);
234
+ if (slackSuccess)
235
+ slack_sent++;
236
+ // Send email to each approver
237
+ for (const approver of approvers) {
238
+ const emailSuccess = await notifyApprovalRequestEmail(approver.email, approver.name, session_id, request_id, requester_name, action_count);
239
+ if (emailSuccess)
240
+ email_sent++;
241
+ }
242
+ return { slack_sent, email_sent };
243
+ }
244
+ /**
245
+ * Broadcast approval decision to all stakeholders
246
+ */
247
+ async function broadcastApprovalDecision(approver, decision, session_id, requester_email, reason) {
248
+ let slack_sent = 0;
249
+ let email_sent = 0;
250
+ // Send Slack notification
251
+ const slackSuccess = await notifyApprovalDecisionSlack(approver.name, decision, session_id, reason);
252
+ if (slackSuccess)
253
+ slack_sent++;
254
+ // Send email to requester
255
+ const emailSuccess = await notifyApprovalDecisionEmail(requester_email, approver.name, decision, session_id, reason);
256
+ if (emailSuccess)
257
+ email_sent++;
258
+ return { slack_sent, email_sent };
259
+ }
260
+ /**
261
+ * Phase 3: Escalation Notifications
262
+ */
263
+ /**
264
+ * Send Slack escalation notification
265
+ */
266
+ async function notifyEscalationSlack(escalation_targets, session_id, request_id, requester_name, escalation_deadline) {
267
+ const config = loadNotificationConfig();
268
+ const slackConfig = config.slack;
269
+ if (!slackConfig?.enabled || !slackConfig.webhook_url) {
270
+ console.log('[Notifications] Slack disabled or unconfigured');
271
+ return false;
272
+ }
273
+ try {
274
+ const deadlineTime = new Date(escalation_deadline).toLocaleString();
275
+ const targetsList = escalation_targets.join(', ');
276
+ const message = {
277
+ text: `⏰ Escalation needed for ${requester_name}'s rollback request`,
278
+ blocks: [
279
+ {
280
+ type: 'section',
281
+ text: {
282
+ type: 'mrkdwn',
283
+ text: `*Approval Escalation* ⏰\n\nApproval for *${requester_name}*'s rollback is stalled.\n\n*Session:* \`${session_id}\`\n*Escalation Targets:* ${targetsList}\n*Decision Deadline:* ${deadlineTime}`,
284
+ },
285
+ },
286
+ {
287
+ type: 'actions',
288
+ elements: [
289
+ {
290
+ type: 'button',
291
+ text: {
292
+ type: 'plain_text',
293
+ text: '✅ Proceed with Rollback',
294
+ },
295
+ value: request_id,
296
+ action_id: 'waymark_escalation_proceed',
297
+ style: 'primary',
298
+ },
299
+ {
300
+ type: 'button',
301
+ text: {
302
+ type: 'plain_text',
303
+ text: '❌ Block Rollback',
304
+ },
305
+ value: request_id,
306
+ action_id: 'waymark_escalation_block',
307
+ style: 'danger',
308
+ },
309
+ ],
310
+ },
311
+ ],
312
+ };
313
+ const response = await fetch(slackConfig.webhook_url, {
314
+ method: 'POST',
315
+ headers: { 'Content-Type': 'application/json' },
316
+ body: JSON.stringify(message),
317
+ });
318
+ return response.ok;
319
+ }
320
+ catch (err) {
321
+ console.error('[Notifications] Slack escalation error:', err);
322
+ return false;
323
+ }
324
+ }
325
+ /**
326
+ * Send Slack escalation decision notification
327
+ */
328
+ async function notifyEscalationDecisionSlack(target_name, decision, session_id, reason) {
329
+ const config = loadNotificationConfig();
330
+ const slackConfig = config.slack;
331
+ if (!slackConfig?.enabled || !slackConfig.webhook_url) {
332
+ return false;
333
+ }
334
+ try {
335
+ const emoji = decision === 'proceed' ? '✅' : '❌';
336
+ const action = decision === 'proceed' ? 'Allowed' : 'Blocked';
337
+ const reasonText = reason ? `\n*Reason:* ${reason}` : '';
338
+ const message = {
339
+ text: `${emoji} Escalation ${action} by ${target_name}`,
340
+ blocks: [
341
+ {
342
+ type: 'section',
343
+ text: {
344
+ type: 'mrkdwn',
345
+ text: `${emoji} *Escalation ${action}*\n\nTarget: *${target_name}*\nSession: \`${session_id}\`${reasonText}`,
346
+ },
347
+ },
348
+ ],
349
+ };
350
+ const response = await fetch(slackConfig.webhook_url, {
351
+ method: 'POST',
352
+ headers: { 'Content-Type': 'application/json' },
353
+ body: JSON.stringify(message),
354
+ });
355
+ return response.ok;
356
+ }
357
+ catch (err) {
358
+ console.error('[Notifications] Slack escalation decision error:', err);
359
+ return false;
360
+ }
361
+ }
362
+ /**
363
+ * Send email escalation notification
364
+ */
365
+ async function notifyEscalationEmail(recipient_email, recipient_name, session_id, request_id, requester_name, escalation_deadline) {
366
+ const config = loadNotificationConfig();
367
+ const emailConfig = config.email;
368
+ if (!emailConfig?.enabled) {
369
+ console.log('[Notifications] Email disabled or unconfigured');
370
+ return false;
371
+ }
372
+ try {
373
+ const deadlineTime = new Date(escalation_deadline).toLocaleString();
374
+ console.log(`[Notifications] Email escalation to ${recipient_email}:
375
+ Recipient: ${recipient_name}
376
+ Requester: ${requester_name}
377
+ Session: ${session_id}
378
+ Request ID: ${request_id}
379
+ Deadline: ${deadlineTime}`);
380
+ return true;
381
+ }
382
+ catch (err) {
383
+ console.error('[Notifications] Email escalation error:', err);
384
+ return false;
385
+ }
386
+ }
387
+ /**
388
+ * Send email escalation decision notification
389
+ */
390
+ async function notifyEscalationDecisionEmail(recipient_email, target_name, decision, session_id, reason) {
391
+ const config = loadNotificationConfig();
392
+ const emailConfig = config.email;
393
+ if (!emailConfig?.enabled) {
394
+ return false;
395
+ }
396
+ try {
397
+ const action = decision === 'proceed' ? 'Allowed' : 'Blocked';
398
+ const reasonText = reason ? `\n\nReason: ${reason}` : '';
399
+ console.log(`[Notifications] Email escalation decision to ${recipient_email}:
400
+ Target: ${target_name}
401
+ Decision: ${action}
402
+ Session: ${session_id}${reasonText}`);
403
+ return true;
404
+ }
405
+ catch (err) {
406
+ console.error('[Notifications] Email escalation decision error:', err);
407
+ return false;
408
+ }
409
+ }
410
+ /**
411
+ * Broadcast escalation request to all targets
412
+ */
413
+ async function broadcastEscalation(targets, session_id, request_id, requester_name, escalation_deadline) {
414
+ let slack_sent = 0;
415
+ let email_sent = 0;
416
+ // Send Slack notification (once to all targets)
417
+ const slackSuccess = await notifyEscalationSlack(targets.map(t => t.member_id), session_id, request_id, requester_name, escalation_deadline);
418
+ if (slackSuccess)
419
+ slack_sent++;
420
+ // Send email to each target
421
+ for (const target of targets) {
422
+ const emailSuccess = await notifyEscalationEmail(target.email, target.name, session_id, request_id, requester_name, escalation_deadline);
423
+ if (emailSuccess)
424
+ email_sent++;
425
+ }
426
+ return { slack_sent, email_sent };
427
+ }
428
+ /**
429
+ * Broadcast escalation decision to all stakeholders
430
+ */
431
+ async function broadcastEscalationDecision(target, decision, session_id, requester_email, reason) {
432
+ let slack_sent = 0;
433
+ let email_sent = 0;
434
+ // Send Slack notification
435
+ const slackSuccess = await notifyEscalationDecisionSlack(target.name, decision, session_id, reason);
436
+ if (slackSuccess)
437
+ slack_sent++;
438
+ // Send email to requester
439
+ const emailSuccess = await notifyEscalationDecisionEmail(requester_email, target.name, decision, session_id, reason);
440
+ if (emailSuccess)
441
+ email_sent++;
442
+ return { slack_sent, email_sent };
443
+ }
444
+ exports.default = {
445
+ notifyApprovalRequestSlack,
446
+ notifyApprovalDecisionSlack,
447
+ notifyApprovalRequestEmail,
448
+ notifyApprovalDecisionEmail,
449
+ broadcastApprovalRequest,
450
+ broadcastApprovalDecision,
451
+ notifyEscalationSlack,
452
+ notifyEscalationDecisionSlack,
453
+ notifyEscalationEmail,
454
+ notifyEscalationDecisionEmail,
455
+ broadcastEscalation,
456
+ broadcastEscalationDecision,
457
+ };