@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
package/dist/db/database.js
CHANGED
|
@@ -49,6 +49,40 @@ exports.getActionsWithFiltering = getActionsWithFiltering;
|
|
|
49
49
|
exports.archiveOldActions = archiveOldActions;
|
|
50
50
|
exports.getArchivedActionsWithFiltering = getArchivedActionsWithFiltering;
|
|
51
51
|
exports.getSummaryStats = getSummaryStats;
|
|
52
|
+
exports.createSession = createSession;
|
|
53
|
+
exports.getSession = getSession;
|
|
54
|
+
exports.getAllSessions = getAllSessions;
|
|
55
|
+
exports.getSessionActions = getSessionActions;
|
|
56
|
+
exports.rollbackSession = rollbackSession;
|
|
57
|
+
exports.markSessionRolledBack = markSessionRolledBack;
|
|
58
|
+
exports.addTeamMember = addTeamMember;
|
|
59
|
+
exports.getTeamMember = getTeamMember;
|
|
60
|
+
exports.getTeamMemberByEmail = getTeamMemberByEmail;
|
|
61
|
+
exports.getAllTeamMembers = getAllTeamMembers;
|
|
62
|
+
exports.removeTeamMember = removeTeamMember;
|
|
63
|
+
exports.addApprovalRoute = addApprovalRoute;
|
|
64
|
+
exports.getApprovalRoute = getApprovalRoute;
|
|
65
|
+
exports.getAllApprovalRoutes = getAllApprovalRoutes;
|
|
66
|
+
exports.updateApprovalRoute = updateApprovalRoute;
|
|
67
|
+
exports.deleteApprovalRoute = deleteApprovalRoute;
|
|
68
|
+
exports.createApprovalRequest = createApprovalRequest;
|
|
69
|
+
exports.getApprovalRequest = getApprovalRequest;
|
|
70
|
+
exports.getSessionApprovalRequests = getSessionApprovalRequests;
|
|
71
|
+
exports.getPendingApprovals = getPendingApprovals;
|
|
72
|
+
exports.submitApprovalDecision = submitApprovalDecision;
|
|
73
|
+
exports.getApprovalDecisions = getApprovalDecisions;
|
|
74
|
+
exports.addEscalationRule = addEscalationRule;
|
|
75
|
+
exports.getEscalationRule = getEscalationRule;
|
|
76
|
+
exports.getAllEscalationRules = getAllEscalationRules;
|
|
77
|
+
exports.updateEscalationRule = updateEscalationRule;
|
|
78
|
+
exports.deleteEscalationRule = deleteEscalationRule;
|
|
79
|
+
exports.createEscalationRequest = createEscalationRequest;
|
|
80
|
+
exports.getEscalationRequest = getEscalationRequest;
|
|
81
|
+
exports.getPendingEscalations = getPendingEscalations;
|
|
82
|
+
exports.getStaleApprovals = getStaleApprovals;
|
|
83
|
+
exports.submitEscalationDecision = submitEscalationDecision;
|
|
84
|
+
exports.getEscalationDecisions = getEscalationDecisions;
|
|
85
|
+
exports.getEscalationHistory = getEscalationHistory;
|
|
52
86
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
53
87
|
const fs = __importStar(require("fs"));
|
|
54
88
|
const path = __importStar(require("path"));
|
|
@@ -56,169 +90,403 @@ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
|
56
90
|
const DB_PATH = process.env.WAYMARK_DB_PATH
|
|
57
91
|
|| path.join(PROJECT_ROOT, '.waymark', 'waymark.db');
|
|
58
92
|
const DB_DIR = path.dirname(DB_PATH);
|
|
59
|
-
//
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
//
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
93
|
+
// Lazy-load database to allow tests to mock it
|
|
94
|
+
let db = null;
|
|
95
|
+
let initialized = false;
|
|
96
|
+
function initializeSchema(database) {
|
|
97
|
+
if (initialized)
|
|
98
|
+
return;
|
|
99
|
+
initialized = true;
|
|
100
|
+
// Create table on startup
|
|
101
|
+
database.exec(`
|
|
102
|
+
CREATE TABLE IF NOT EXISTS action_log (
|
|
103
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
104
|
+
action_id TEXT UNIQUE NOT NULL,
|
|
105
|
+
session_id TEXT NOT NULL,
|
|
106
|
+
tool_name TEXT NOT NULL,
|
|
107
|
+
target_path TEXT,
|
|
108
|
+
input_payload TEXT NOT NULL,
|
|
109
|
+
before_snapshot TEXT,
|
|
110
|
+
after_snapshot TEXT,
|
|
111
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
112
|
+
error_message TEXT,
|
|
113
|
+
stdout TEXT,
|
|
114
|
+
stderr TEXT,
|
|
115
|
+
rolled_back INTEGER NOT NULL DEFAULT 0,
|
|
116
|
+
rolled_back_at TEXT,
|
|
117
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
118
|
+
)
|
|
119
|
+
`);
|
|
120
|
+
// Migrate existing DBs — add stdout/stderr if not present
|
|
121
|
+
try {
|
|
122
|
+
database.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
|
|
123
|
+
}
|
|
124
|
+
catch { }
|
|
125
|
+
try {
|
|
126
|
+
database.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
|
|
127
|
+
}
|
|
128
|
+
catch { }
|
|
129
|
+
// Migrate v2: policy engine columns
|
|
130
|
+
try {
|
|
131
|
+
database.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
|
|
132
|
+
}
|
|
133
|
+
catch { }
|
|
134
|
+
try {
|
|
135
|
+
database.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
|
|
136
|
+
}
|
|
137
|
+
catch { }
|
|
138
|
+
try {
|
|
139
|
+
database.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
|
|
140
|
+
}
|
|
141
|
+
catch { }
|
|
142
|
+
// Migrate v3: approval flow columns
|
|
143
|
+
try {
|
|
144
|
+
database.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
|
|
145
|
+
}
|
|
146
|
+
catch { }
|
|
147
|
+
try {
|
|
148
|
+
database.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
|
|
149
|
+
}
|
|
150
|
+
catch { }
|
|
151
|
+
try {
|
|
152
|
+
database.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
|
|
153
|
+
}
|
|
154
|
+
catch { }
|
|
155
|
+
try {
|
|
156
|
+
database.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
// Migrate v4: Phase 1 — plan mode logging visibility
|
|
160
|
+
try {
|
|
161
|
+
database.exec("ALTER TABLE action_log ADD COLUMN event_type TEXT NOT NULL DEFAULT 'execution'");
|
|
162
|
+
}
|
|
163
|
+
catch { }
|
|
164
|
+
try {
|
|
165
|
+
database.exec('ALTER TABLE action_log ADD COLUMN observation_context TEXT');
|
|
166
|
+
}
|
|
167
|
+
catch { }
|
|
168
|
+
try {
|
|
169
|
+
database.exec('ALTER TABLE action_log ADD COLUMN request_source TEXT DEFAULT \'direct\'');
|
|
170
|
+
}
|
|
171
|
+
catch { }
|
|
172
|
+
// Migrate v5: Phase 5B — CLI action logging (GitHub Copilot CLI wrapper)
|
|
173
|
+
try {
|
|
174
|
+
database.exec("ALTER TABLE action_log ADD COLUMN source TEXT DEFAULT 'mcp'");
|
|
175
|
+
}
|
|
176
|
+
catch { }
|
|
177
|
+
// Migrate v6: Phase 1 — Session-level rollback
|
|
178
|
+
try {
|
|
179
|
+
database.exec('ALTER TABLE action_log ADD COLUMN rollback_group TEXT');
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
try {
|
|
183
|
+
database.exec('ALTER TABLE action_log ADD COLUMN is_reversible INTEGER DEFAULT 1');
|
|
184
|
+
}
|
|
185
|
+
catch { }
|
|
186
|
+
try {
|
|
187
|
+
database.exec('ALTER TABLE action_log ADD COLUMN revert_action_id TEXT');
|
|
188
|
+
}
|
|
189
|
+
catch { }
|
|
190
|
+
// Phase 1: Create sessions table
|
|
191
|
+
database.exec(`
|
|
192
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
193
|
+
session_id TEXT PRIMARY KEY,
|
|
194
|
+
user_id TEXT,
|
|
195
|
+
project_id TEXT,
|
|
196
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
197
|
+
rolled_back_at DATETIME,
|
|
198
|
+
status TEXT NOT NULL DEFAULT 'active'
|
|
199
|
+
)
|
|
200
|
+
`);
|
|
201
|
+
// Indexes for query performance
|
|
202
|
+
try {
|
|
203
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_action_id ON action_log(action_id)');
|
|
204
|
+
}
|
|
205
|
+
catch { }
|
|
206
|
+
try {
|
|
207
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_status ON action_log(status)');
|
|
208
|
+
}
|
|
209
|
+
catch { }
|
|
210
|
+
try {
|
|
211
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_session_id ON action_log(session_id)');
|
|
212
|
+
}
|
|
213
|
+
catch { }
|
|
214
|
+
// Phase 3: Add indexes for pagination and filtering
|
|
215
|
+
try {
|
|
216
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_tool_name ON action_log(tool_name)');
|
|
217
|
+
}
|
|
218
|
+
catch { }
|
|
219
|
+
try {
|
|
220
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_created_at ON action_log(created_at DESC)');
|
|
221
|
+
}
|
|
222
|
+
catch { }
|
|
223
|
+
try {
|
|
224
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_status_created ON action_log(status, created_at DESC)');
|
|
225
|
+
}
|
|
226
|
+
catch { }
|
|
227
|
+
// Phase 3: Create archive table (same schema as action_log)
|
|
228
|
+
database.exec(`
|
|
229
|
+
CREATE TABLE IF NOT EXISTS action_archive (
|
|
230
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
231
|
+
action_id TEXT UNIQUE NOT NULL,
|
|
232
|
+
session_id TEXT NOT NULL,
|
|
233
|
+
tool_name TEXT NOT NULL,
|
|
234
|
+
target_path TEXT,
|
|
235
|
+
input_payload TEXT NOT NULL,
|
|
236
|
+
before_snapshot TEXT,
|
|
237
|
+
after_snapshot TEXT,
|
|
238
|
+
status TEXT NOT NULL DEFAULT 'pending',
|
|
239
|
+
error_message TEXT,
|
|
240
|
+
stdout TEXT,
|
|
241
|
+
stderr TEXT,
|
|
242
|
+
rolled_back INTEGER NOT NULL DEFAULT 0,
|
|
243
|
+
rolled_back_at TEXT,
|
|
244
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
245
|
+
decision TEXT NOT NULL DEFAULT 'allow',
|
|
246
|
+
policy_reason TEXT,
|
|
247
|
+
matched_rule TEXT,
|
|
248
|
+
approved_at TEXT,
|
|
249
|
+
approved_by TEXT,
|
|
250
|
+
rejected_at TEXT,
|
|
251
|
+
rejected_reason TEXT,
|
|
252
|
+
event_type TEXT NOT NULL DEFAULT 'execution',
|
|
253
|
+
observation_context TEXT,
|
|
254
|
+
request_source TEXT DEFAULT 'direct',
|
|
255
|
+
source TEXT DEFAULT 'mcp',
|
|
256
|
+
rollback_group TEXT,
|
|
257
|
+
is_reversible INTEGER DEFAULT 1,
|
|
258
|
+
revert_action_id TEXT
|
|
259
|
+
)
|
|
260
|
+
`);
|
|
261
|
+
// Phase 1: Add same columns to archive (migrations)
|
|
262
|
+
try {
|
|
263
|
+
database.exec('ALTER TABLE action_archive ADD COLUMN rollback_group TEXT');
|
|
264
|
+
}
|
|
265
|
+
catch { }
|
|
266
|
+
try {
|
|
267
|
+
database.exec('ALTER TABLE action_archive ADD COLUMN is_reversible INTEGER DEFAULT 1');
|
|
268
|
+
}
|
|
269
|
+
catch { }
|
|
270
|
+
try {
|
|
271
|
+
database.exec('ALTER TABLE action_archive ADD COLUMN revert_action_id TEXT');
|
|
272
|
+
}
|
|
273
|
+
catch { }
|
|
274
|
+
// Phase 3: Add indexes on archive table
|
|
275
|
+
try {
|
|
276
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_archive_action_id ON action_archive(action_id)');
|
|
277
|
+
}
|
|
278
|
+
catch { }
|
|
279
|
+
try {
|
|
280
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_archive_created_at ON action_archive(created_at DESC)');
|
|
281
|
+
}
|
|
282
|
+
catch { }
|
|
283
|
+
// Phase 1: Add indexes for session rollback
|
|
284
|
+
try {
|
|
285
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_action_session ON action_log(session_id, status)');
|
|
286
|
+
}
|
|
287
|
+
catch { }
|
|
288
|
+
try {
|
|
289
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_action_rollback_group ON action_log(rollback_group)');
|
|
290
|
+
}
|
|
291
|
+
catch { }
|
|
292
|
+
try {
|
|
293
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status, created_at DESC)');
|
|
294
|
+
}
|
|
295
|
+
catch { }
|
|
296
|
+
try {
|
|
297
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_archive_session ON action_archive(session_id)');
|
|
298
|
+
}
|
|
299
|
+
catch { }
|
|
300
|
+
// Phase 2: Create team_members table
|
|
301
|
+
database.exec(`
|
|
302
|
+
CREATE TABLE IF NOT EXISTS team_members (
|
|
303
|
+
member_id TEXT PRIMARY KEY,
|
|
304
|
+
name TEXT NOT NULL,
|
|
305
|
+
email TEXT NOT NULL UNIQUE,
|
|
306
|
+
slack_id TEXT,
|
|
307
|
+
role TEXT DEFAULT 'approver',
|
|
308
|
+
added_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
309
|
+
added_by TEXT,
|
|
310
|
+
status TEXT DEFAULT 'active'
|
|
311
|
+
)
|
|
312
|
+
`);
|
|
313
|
+
// Phase 2: Create approval_routes table (rules for who approves what)
|
|
314
|
+
database.exec(`
|
|
315
|
+
CREATE TABLE IF NOT EXISTS approval_routes (
|
|
316
|
+
route_id TEXT PRIMARY KEY,
|
|
317
|
+
name TEXT NOT NULL,
|
|
318
|
+
description TEXT,
|
|
319
|
+
condition_type TEXT DEFAULT 'all_sessions',
|
|
320
|
+
condition_json TEXT,
|
|
321
|
+
required_approvers INTEGER DEFAULT 1,
|
|
322
|
+
approver_ids TEXT NOT NULL,
|
|
323
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
324
|
+
created_by TEXT,
|
|
325
|
+
status TEXT DEFAULT 'active'
|
|
326
|
+
)
|
|
327
|
+
`);
|
|
328
|
+
// Phase 2: Create approval_requests table (pending/completed approvals)
|
|
329
|
+
database.exec(`
|
|
330
|
+
CREATE TABLE IF NOT EXISTS approval_requests (
|
|
331
|
+
request_id TEXT PRIMARY KEY,
|
|
332
|
+
session_id TEXT NOT NULL,
|
|
333
|
+
route_id TEXT NOT NULL,
|
|
334
|
+
triggered_by TEXT NOT NULL,
|
|
335
|
+
triggered_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
336
|
+
status TEXT DEFAULT 'pending',
|
|
337
|
+
completed_at DATETIME,
|
|
338
|
+
approver_ids TEXT NOT NULL,
|
|
339
|
+
approved_count INTEGER DEFAULT 0,
|
|
340
|
+
rejected_count INTEGER DEFAULT 0,
|
|
341
|
+
approval_details TEXT
|
|
342
|
+
)
|
|
343
|
+
`);
|
|
344
|
+
// Phase 2: Create approval_decisions table (audit trail)
|
|
345
|
+
database.exec(`
|
|
346
|
+
CREATE TABLE IF NOT EXISTS approval_decisions (
|
|
347
|
+
decision_id TEXT PRIMARY KEY,
|
|
348
|
+
request_id TEXT NOT NULL,
|
|
349
|
+
approver_id TEXT NOT NULL,
|
|
350
|
+
decision TEXT NOT NULL,
|
|
351
|
+
reason TEXT,
|
|
352
|
+
decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
353
|
+
)
|
|
354
|
+
`);
|
|
355
|
+
// Phase 2: Add indexes for team tables
|
|
356
|
+
try {
|
|
357
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_email ON team_members(email)');
|
|
358
|
+
}
|
|
359
|
+
catch { }
|
|
360
|
+
try {
|
|
361
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_team_members_status ON team_members(status)');
|
|
362
|
+
}
|
|
363
|
+
catch { }
|
|
364
|
+
try {
|
|
365
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_routes_status ON approval_routes(status)');
|
|
366
|
+
}
|
|
367
|
+
catch { }
|
|
368
|
+
try {
|
|
369
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_session ON approval_requests(session_id)');
|
|
370
|
+
}
|
|
371
|
+
catch { }
|
|
372
|
+
try {
|
|
373
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status)');
|
|
374
|
+
}
|
|
375
|
+
catch { }
|
|
376
|
+
try {
|
|
377
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_requests_triggered ON approval_requests(triggered_at DESC)');
|
|
378
|
+
}
|
|
379
|
+
catch { }
|
|
380
|
+
try {
|
|
381
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_request ON approval_decisions(request_id)');
|
|
382
|
+
}
|
|
383
|
+
catch { }
|
|
384
|
+
try {
|
|
385
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_approval_decisions_approver ON approval_decisions(approver_id)');
|
|
386
|
+
}
|
|
387
|
+
catch { }
|
|
388
|
+
// Phase 3: Create escalation_rules table
|
|
389
|
+
database.exec(`
|
|
390
|
+
CREATE TABLE IF NOT EXISTS escalation_rules (
|
|
391
|
+
rule_id TEXT PRIMARY KEY,
|
|
392
|
+
name TEXT NOT NULL,
|
|
393
|
+
description TEXT,
|
|
394
|
+
timeout_hours INTEGER DEFAULT 24,
|
|
395
|
+
escalation_targets TEXT NOT NULL,
|
|
396
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
397
|
+
created_by TEXT,
|
|
398
|
+
status TEXT DEFAULT 'active'
|
|
399
|
+
)
|
|
400
|
+
`);
|
|
401
|
+
// Phase 3: Create escalation_requests table
|
|
402
|
+
database.exec(`
|
|
403
|
+
CREATE TABLE IF NOT EXISTS escalation_requests (
|
|
404
|
+
request_id TEXT PRIMARY KEY,
|
|
405
|
+
approval_request_id TEXT NOT NULL,
|
|
406
|
+
session_id TEXT NOT NULL,
|
|
407
|
+
escalation_triggered_at DATETIME,
|
|
408
|
+
escalation_deadline DATETIME,
|
|
409
|
+
escalation_targets TEXT NOT NULL,
|
|
410
|
+
status TEXT DEFAULT 'pending',
|
|
411
|
+
decided_at DATETIME,
|
|
412
|
+
decision TEXT
|
|
413
|
+
)
|
|
414
|
+
`);
|
|
415
|
+
// Phase 3: Create escalation_decisions table
|
|
416
|
+
database.exec(`
|
|
417
|
+
CREATE TABLE IF NOT EXISTS escalation_decisions (
|
|
418
|
+
decision_id TEXT PRIMARY KEY,
|
|
419
|
+
escalation_request_id TEXT NOT NULL,
|
|
420
|
+
target_id TEXT NOT NULL,
|
|
421
|
+
decision TEXT NOT NULL,
|
|
422
|
+
reason TEXT,
|
|
423
|
+
decided_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
424
|
+
)
|
|
425
|
+
`);
|
|
426
|
+
// Phase 3: Add indexes for escalation tables
|
|
427
|
+
try {
|
|
428
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_rules_status ON escalation_rules(status)');
|
|
429
|
+
}
|
|
430
|
+
catch { }
|
|
431
|
+
try {
|
|
432
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_approval ON escalation_requests(approval_request_id)');
|
|
433
|
+
}
|
|
434
|
+
catch { }
|
|
435
|
+
try {
|
|
436
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_deadline ON escalation_requests(escalation_deadline)');
|
|
437
|
+
}
|
|
438
|
+
catch { }
|
|
439
|
+
try {
|
|
440
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_requests_status ON escalation_requests(status)');
|
|
441
|
+
}
|
|
442
|
+
catch { }
|
|
443
|
+
try {
|
|
444
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_request ON escalation_decisions(escalation_request_id)');
|
|
445
|
+
}
|
|
446
|
+
catch { }
|
|
447
|
+
try {
|
|
448
|
+
database.exec('CREATE INDEX IF NOT EXISTS idx_escalation_decisions_target ON escalation_decisions(target_id)');
|
|
449
|
+
}
|
|
450
|
+
catch { }
|
|
451
|
+
}
|
|
452
|
+
function getDb() {
|
|
453
|
+
if (!db) {
|
|
454
|
+
// Ensure database directory exists
|
|
455
|
+
if (!fs.existsSync(DB_DIR)) {
|
|
456
|
+
fs.mkdirSync(DB_DIR, { recursive: true });
|
|
457
|
+
}
|
|
458
|
+
db = new better_sqlite3_1.default(DB_PATH);
|
|
459
|
+
initializeSchema(db);
|
|
460
|
+
}
|
|
461
|
+
return db;
|
|
462
|
+
}
|
|
463
|
+
let insertStmt = null;
|
|
464
|
+
let updateStmt = null;
|
|
465
|
+
function getInsertStmt() {
|
|
466
|
+
if (!insertStmt) {
|
|
467
|
+
insertStmt = getDb().prepare(`
|
|
468
|
+
INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule, event_type, observation_context, request_source, source)
|
|
469
|
+
VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule, @event_type, @observation_context, @request_source, @source)
|
|
470
|
+
`);
|
|
471
|
+
}
|
|
472
|
+
return insertStmt;
|
|
473
|
+
}
|
|
474
|
+
function getUpdateStmt() {
|
|
475
|
+
if (!updateStmt) {
|
|
476
|
+
updateStmt = getDb().prepare(`
|
|
477
|
+
UPDATE action_log
|
|
478
|
+
SET status = COALESCE(@status, status),
|
|
479
|
+
after_snapshot = COALESCE(@after_snapshot, after_snapshot),
|
|
480
|
+
error_message = COALESCE(@error_message, error_message),
|
|
481
|
+
stdout = COALESCE(@stdout, stdout),
|
|
482
|
+
stderr = COALESCE(@stderr, stderr)
|
|
483
|
+
WHERE action_id = @action_id
|
|
484
|
+
`);
|
|
485
|
+
}
|
|
486
|
+
return updateStmt;
|
|
487
|
+
}
|
|
220
488
|
function insertAction(params) {
|
|
221
|
-
|
|
489
|
+
getInsertStmt().run({
|
|
222
490
|
action_id: params.action_id,
|
|
223
491
|
session_id: params.session_id,
|
|
224
492
|
tool_name: params.tool_name,
|
|
@@ -236,7 +504,7 @@ function insertAction(params) {
|
|
|
236
504
|
});
|
|
237
505
|
}
|
|
238
506
|
function updateAction(action_id, params) {
|
|
239
|
-
|
|
507
|
+
getUpdateStmt().run({
|
|
240
508
|
action_id,
|
|
241
509
|
status: params.status ?? null,
|
|
242
510
|
after_snapshot: params.after_snapshot ?? null,
|
|
@@ -246,24 +514,24 @@ function updateAction(action_id, params) {
|
|
|
246
514
|
});
|
|
247
515
|
}
|
|
248
516
|
function getActions() {
|
|
249
|
-
return
|
|
517
|
+
return getDb().prepare(`
|
|
250
518
|
SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
|
|
251
519
|
`).all();
|
|
252
520
|
}
|
|
253
521
|
function getAction(action_id) {
|
|
254
|
-
return
|
|
522
|
+
return getDb().prepare(`
|
|
255
523
|
SELECT * FROM action_log WHERE action_id = ?
|
|
256
524
|
`).get(action_id);
|
|
257
525
|
}
|
|
258
526
|
function markRolledBack(action_id) {
|
|
259
|
-
|
|
527
|
+
getDb().prepare(`
|
|
260
528
|
UPDATE action_log
|
|
261
529
|
SET rolled_back = 1, rolled_back_at = datetime('now')
|
|
262
530
|
WHERE action_id = ?
|
|
263
531
|
`).run(action_id);
|
|
264
532
|
}
|
|
265
533
|
function getSessions() {
|
|
266
|
-
return
|
|
534
|
+
return getDb().prepare(`
|
|
267
535
|
SELECT session_id,
|
|
268
536
|
COUNT(*) as action_count,
|
|
269
537
|
MAX(created_at) as latest
|
|
@@ -273,7 +541,7 @@ function getSessions() {
|
|
|
273
541
|
`).all();
|
|
274
542
|
}
|
|
275
543
|
function approveAction(action_id, approved_by, after_snapshot) {
|
|
276
|
-
|
|
544
|
+
getDb().prepare(`
|
|
277
545
|
UPDATE action_log
|
|
278
546
|
SET status = 'success',
|
|
279
547
|
decision = 'allow',
|
|
@@ -284,7 +552,7 @@ function approveAction(action_id, approved_by, after_snapshot) {
|
|
|
284
552
|
`).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
|
|
285
553
|
}
|
|
286
554
|
function rejectAction(action_id, reason) {
|
|
287
|
-
|
|
555
|
+
getDb().prepare(`
|
|
288
556
|
UPDATE action_log
|
|
289
557
|
SET status = 'rejected',
|
|
290
558
|
decision = 'rejected',
|
|
@@ -294,7 +562,7 @@ function rejectAction(action_id, reason) {
|
|
|
294
562
|
`).run({ action_id, reason });
|
|
295
563
|
}
|
|
296
564
|
function getPendingCount() {
|
|
297
|
-
const row =
|
|
565
|
+
const row = getDb().prepare(`
|
|
298
566
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
|
|
299
567
|
`).get();
|
|
300
568
|
return row.count;
|
|
@@ -318,12 +586,12 @@ function getActionsWithFiltering(filter = {}) {
|
|
|
318
586
|
params.search = `%${filter.search}%`;
|
|
319
587
|
}
|
|
320
588
|
// Get total count
|
|
321
|
-
const countRow =
|
|
589
|
+
const countRow = getDb().prepare(`
|
|
322
590
|
SELECT COUNT(*) as count FROM action_log WHERE ${where}
|
|
323
591
|
`).get(params);
|
|
324
592
|
const totalCount = countRow.count;
|
|
325
593
|
// Get paginated results
|
|
326
|
-
const actions =
|
|
594
|
+
const actions = getDb().prepare(`
|
|
327
595
|
SELECT * FROM action_log WHERE ${where}
|
|
328
596
|
ORDER BY created_at DESC
|
|
329
597
|
LIMIT @limit OFFSET @offset
|
|
@@ -341,12 +609,12 @@ function archiveOldActions(daysOld = 30) {
|
|
|
341
609
|
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
342
610
|
const cutoffStr = cutoffDate.toISOString();
|
|
343
611
|
// Copy old actions to archive
|
|
344
|
-
const result =
|
|
612
|
+
const result = getDb().prepare(`
|
|
345
613
|
INSERT OR IGNORE INTO action_archive
|
|
346
614
|
SELECT * FROM action_log WHERE created_at < @cutoff
|
|
347
615
|
`).run({ cutoff: cutoffStr });
|
|
348
616
|
// Delete from main table (but keep recent entries)
|
|
349
|
-
const deleteStmt =
|
|
617
|
+
const deleteStmt = getDb().prepare(`
|
|
350
618
|
DELETE FROM action_log WHERE created_at < @cutoff
|
|
351
619
|
AND id NOT IN (
|
|
352
620
|
SELECT id FROM action_log ORDER BY created_at DESC LIMIT 1000
|
|
@@ -373,11 +641,11 @@ function getArchivedActionsWithFiltering(filter = {}) {
|
|
|
373
641
|
where += ' AND (action_id LIKE @search OR target_path LIKE @search)';
|
|
374
642
|
params.search = `%${filter.search}%`;
|
|
375
643
|
}
|
|
376
|
-
const countRow =
|
|
644
|
+
const countRow = getDb().prepare(`
|
|
377
645
|
SELECT COUNT(*) as count FROM action_archive WHERE ${where}
|
|
378
646
|
`).get(params);
|
|
379
647
|
const totalCount = countRow.count;
|
|
380
|
-
const actions =
|
|
648
|
+
const actions = getDb().prepare(`
|
|
381
649
|
SELECT * FROM action_archive WHERE ${where}
|
|
382
650
|
ORDER BY created_at DESC
|
|
383
651
|
LIMIT @limit OFFSET @offset
|
|
@@ -395,34 +663,34 @@ function getSummaryStats() {
|
|
|
395
663
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
|
|
396
664
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
397
665
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
398
|
-
const total =
|
|
666
|
+
const total = getDb().prepare(`
|
|
399
667
|
SELECT COUNT(*) as count FROM action_log
|
|
400
668
|
`).get();
|
|
401
|
-
const pending =
|
|
669
|
+
const pending = getDb().prepare(`
|
|
402
670
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
|
|
403
671
|
`).get();
|
|
404
|
-
const approved =
|
|
672
|
+
const approved = getDb().prepare(`
|
|
405
673
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'success' OR decision = 'allow'
|
|
406
674
|
`).get();
|
|
407
|
-
const rejected =
|
|
675
|
+
const rejected = getDb().prepare(`
|
|
408
676
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'rejected' OR decision = 'rejected'
|
|
409
677
|
`).get();
|
|
410
|
-
const todayCount =
|
|
678
|
+
const todayCount = getDb().prepare(`
|
|
411
679
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @today
|
|
412
680
|
`).get({ today });
|
|
413
|
-
const weekCount =
|
|
681
|
+
const weekCount = getDb().prepare(`
|
|
414
682
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @weekAgo
|
|
415
683
|
`).get({ weekAgo });
|
|
416
|
-
const monthCount =
|
|
684
|
+
const monthCount = getDb().prepare(`
|
|
417
685
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @monthAgo
|
|
418
686
|
`).get({ monthAgo });
|
|
419
|
-
const topTools =
|
|
687
|
+
const topTools = getDb().prepare(`
|
|
420
688
|
SELECT tool_name as tool, COUNT(*) as count FROM action_log
|
|
421
689
|
GROUP BY tool_name
|
|
422
690
|
ORDER BY count DESC
|
|
423
691
|
LIMIT 5
|
|
424
692
|
`).all();
|
|
425
|
-
const topPaths =
|
|
693
|
+
const topPaths = getDb().prepare(`
|
|
426
694
|
SELECT target_path as path, COUNT(*) as count FROM action_log
|
|
427
695
|
WHERE target_path IS NOT NULL
|
|
428
696
|
GROUP BY target_path
|
|
@@ -441,4 +709,305 @@ function getSummaryStats() {
|
|
|
441
709
|
topPaths,
|
|
442
710
|
};
|
|
443
711
|
}
|
|
712
|
+
// Phase 1: Session management functions
|
|
713
|
+
function createSession(session_id, user_id, project_id) {
|
|
714
|
+
getDb().prepare(`
|
|
715
|
+
INSERT OR IGNORE INTO sessions (session_id, user_id, project_id, status)
|
|
716
|
+
VALUES (?, ?, ?, 'active')
|
|
717
|
+
`).run(session_id, user_id ?? null, project_id ?? null);
|
|
718
|
+
}
|
|
719
|
+
function getSession(session_id) {
|
|
720
|
+
return getDb().prepare(`
|
|
721
|
+
SELECT * FROM sessions WHERE session_id = ?
|
|
722
|
+
`).get(session_id);
|
|
723
|
+
}
|
|
724
|
+
function getAllSessions() {
|
|
725
|
+
return getDb().prepare(`
|
|
726
|
+
SELECT * FROM sessions ORDER BY created_at DESC
|
|
727
|
+
`).all();
|
|
728
|
+
}
|
|
729
|
+
function getSessionActions(session_id) {
|
|
730
|
+
return getDb().prepare(`
|
|
731
|
+
SELECT * FROM action_log
|
|
732
|
+
WHERE session_id = ?
|
|
733
|
+
ORDER BY created_at ASC
|
|
734
|
+
`).all(session_id);
|
|
735
|
+
}
|
|
736
|
+
function rollbackSession(session_id) {
|
|
737
|
+
try {
|
|
738
|
+
// Start transaction
|
|
739
|
+
const transaction = getDb().transaction(() => {
|
|
740
|
+
// Get all actions in session
|
|
741
|
+
const actions = getSessionActions(session_id);
|
|
742
|
+
if (actions.length === 0) {
|
|
743
|
+
throw new Error(`Session ${session_id} has no actions to rollback`);
|
|
744
|
+
}
|
|
745
|
+
// Check if all actions are reversible
|
|
746
|
+
const nonReversible = actions.filter(a => a.is_reversible === 0);
|
|
747
|
+
if (nonReversible.length > 0) {
|
|
748
|
+
throw new Error(`${nonReversible.length} action(s) in session are not reversible`);
|
|
749
|
+
}
|
|
750
|
+
// For each action, perform rollback
|
|
751
|
+
let rolledBackCount = 0;
|
|
752
|
+
for (const action of actions) {
|
|
753
|
+
// Mark as rolled back
|
|
754
|
+
markRolledBack(action.action_id);
|
|
755
|
+
rolledBackCount++;
|
|
756
|
+
// If before_snapshot exists, restore it (for write_file actions)
|
|
757
|
+
if (action.before_snapshot) {
|
|
758
|
+
try {
|
|
759
|
+
const snapshot = JSON.parse(action.before_snapshot);
|
|
760
|
+
if (snapshot.file_path && snapshot.content !== undefined) {
|
|
761
|
+
// This will be handled by the rollback manager
|
|
762
|
+
// We just mark it as rolled back here
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
catch { }
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// Mark session as rolled back
|
|
769
|
+
getDb().prepare(`
|
|
770
|
+
UPDATE sessions
|
|
771
|
+
SET rolled_back_at = datetime('now'), status = 'rolled_back'
|
|
772
|
+
WHERE session_id = ?
|
|
773
|
+
`).run(session_id);
|
|
774
|
+
return rolledBackCount;
|
|
775
|
+
});
|
|
776
|
+
const actionsRolledBack = transaction();
|
|
777
|
+
return {
|
|
778
|
+
success: true,
|
|
779
|
+
session_id,
|
|
780
|
+
actions_rolled_back: actionsRolledBack,
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
catch (error) {
|
|
784
|
+
return {
|
|
785
|
+
success: false,
|
|
786
|
+
session_id,
|
|
787
|
+
actions_rolled_back: 0,
|
|
788
|
+
error: error.message,
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function markSessionRolledBack(session_id) {
|
|
793
|
+
getDb().prepare(`
|
|
794
|
+
UPDATE sessions
|
|
795
|
+
SET rolled_back_at = datetime('now'), status = 'rolled_back'
|
|
796
|
+
WHERE session_id = ?
|
|
797
|
+
`).run(session_id);
|
|
798
|
+
}
|
|
799
|
+
// Phase 2: Team member functions
|
|
800
|
+
function addTeamMember(member_id, name, email, added_by, slack_id) {
|
|
801
|
+
getDb().prepare(`
|
|
802
|
+
INSERT OR REPLACE INTO team_members (member_id, name, email, slack_id, added_by, status)
|
|
803
|
+
VALUES (?, ?, ?, ?, ?, 'active')
|
|
804
|
+
`).run(member_id, name, email, slack_id ?? null, added_by);
|
|
805
|
+
}
|
|
806
|
+
function getTeamMember(member_id) {
|
|
807
|
+
return getDb().prepare(`
|
|
808
|
+
SELECT * FROM team_members WHERE member_id = ?
|
|
809
|
+
`).get(member_id);
|
|
810
|
+
}
|
|
811
|
+
function getTeamMemberByEmail(email) {
|
|
812
|
+
return getDb().prepare(`
|
|
813
|
+
SELECT * FROM team_members WHERE email = ?
|
|
814
|
+
`).get(email);
|
|
815
|
+
}
|
|
816
|
+
function getAllTeamMembers() {
|
|
817
|
+
return getDb().prepare(`
|
|
818
|
+
SELECT * FROM team_members WHERE status = 'active' ORDER BY name ASC
|
|
819
|
+
`).all();
|
|
820
|
+
}
|
|
821
|
+
function removeTeamMember(member_id) {
|
|
822
|
+
getDb().prepare(`
|
|
823
|
+
UPDATE team_members SET status = 'inactive' WHERE member_id = ?
|
|
824
|
+
`).run(member_id);
|
|
825
|
+
}
|
|
826
|
+
// Phase 2: Approval route functions
|
|
827
|
+
function addApprovalRoute(route_id, name, approver_ids, created_by, description, condition_type, condition_json) {
|
|
828
|
+
getDb().prepare(`
|
|
829
|
+
INSERT OR REPLACE INTO approval_routes (route_id, name, description, condition_type, condition_json, approver_ids, created_by, status)
|
|
830
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
|
831
|
+
`).run(route_id, name, description ?? null, condition_type ?? 'all_sessions', condition_json ?? null, JSON.stringify(approver_ids), created_by);
|
|
832
|
+
}
|
|
833
|
+
function getApprovalRoute(route_id) {
|
|
834
|
+
return getDb().prepare(`
|
|
835
|
+
SELECT * FROM approval_routes WHERE route_id = ?
|
|
836
|
+
`).get(route_id);
|
|
837
|
+
}
|
|
838
|
+
function getAllApprovalRoutes() {
|
|
839
|
+
return getDb().prepare(`
|
|
840
|
+
SELECT * FROM approval_routes WHERE status = 'active' ORDER BY name ASC
|
|
841
|
+
`).all();
|
|
842
|
+
}
|
|
843
|
+
function updateApprovalRoute(route_id, updates) {
|
|
844
|
+
const current = getApprovalRoute(route_id);
|
|
845
|
+
if (!current)
|
|
846
|
+
throw new Error(`Route ${route_id} not found`);
|
|
847
|
+
getDb().prepare(`
|
|
848
|
+
UPDATE approval_routes
|
|
849
|
+
SET name = ?, description = ?, approver_ids = ?
|
|
850
|
+
WHERE route_id = ?
|
|
851
|
+
`).run(updates.name ?? current.name, updates.description ?? current.description, updates.approver_ids ? JSON.stringify(updates.approver_ids) : current.approver_ids, route_id);
|
|
852
|
+
}
|
|
853
|
+
function deleteApprovalRoute(route_id) {
|
|
854
|
+
getDb().prepare(`
|
|
855
|
+
UPDATE approval_routes SET status = 'inactive' WHERE route_id = ?
|
|
856
|
+
`).run(route_id);
|
|
857
|
+
}
|
|
858
|
+
// Phase 2: Approval request functions
|
|
859
|
+
function createApprovalRequest(request_id, session_id, route_id, triggered_by, approver_ids) {
|
|
860
|
+
getDb().prepare(`
|
|
861
|
+
INSERT INTO approval_requests (request_id, session_id, route_id, triggered_by, approver_ids, status)
|
|
862
|
+
VALUES (?, ?, ?, ?, ?, 'pending')
|
|
863
|
+
`).run(request_id, session_id, route_id, triggered_by, JSON.stringify(approver_ids));
|
|
864
|
+
}
|
|
865
|
+
function getApprovalRequest(request_id) {
|
|
866
|
+
return getDb().prepare(`
|
|
867
|
+
SELECT * FROM approval_requests WHERE request_id = ?
|
|
868
|
+
`).get(request_id);
|
|
869
|
+
}
|
|
870
|
+
function getSessionApprovalRequests(session_id) {
|
|
871
|
+
return getDb().prepare(`
|
|
872
|
+
SELECT * FROM approval_requests WHERE session_id = ? ORDER BY triggered_at DESC
|
|
873
|
+
`).all(session_id);
|
|
874
|
+
}
|
|
875
|
+
function getPendingApprovals(approver_id) {
|
|
876
|
+
if (approver_id) {
|
|
877
|
+
return getDb().prepare(`
|
|
878
|
+
SELECT * FROM approval_requests
|
|
879
|
+
WHERE status = 'pending' AND approver_ids LIKE ?
|
|
880
|
+
ORDER BY triggered_at DESC
|
|
881
|
+
`).all(`%"${approver_id}"%`);
|
|
882
|
+
}
|
|
883
|
+
return getDb().prepare(`
|
|
884
|
+
SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY triggered_at DESC
|
|
885
|
+
`).all();
|
|
886
|
+
}
|
|
887
|
+
function submitApprovalDecision(decision_id, request_id, approver_id, decision, reason) {
|
|
888
|
+
// Insert decision
|
|
889
|
+
getDb().prepare(`
|
|
890
|
+
INSERT INTO approval_decisions (decision_id, request_id, approver_id, decision, reason)
|
|
891
|
+
VALUES (?, ?, ?, ?, ?)
|
|
892
|
+
`).run(decision_id, request_id, approver_id, decision, reason ?? null);
|
|
893
|
+
// Update approval request counts
|
|
894
|
+
const request = getApprovalRequest(request_id);
|
|
895
|
+
if (!request)
|
|
896
|
+
throw new Error(`Request ${request_id} not found`);
|
|
897
|
+
const decisions = getDb().prepare(`
|
|
898
|
+
SELECT decision FROM approval_decisions WHERE request_id = ?
|
|
899
|
+
`).all(request_id);
|
|
900
|
+
const approvedCount = decisions.filter(d => d.decision === 'approve').length;
|
|
901
|
+
const rejectedCount = decisions.filter(d => d.decision === 'reject').length;
|
|
902
|
+
const approverIds = JSON.parse(request.approver_ids);
|
|
903
|
+
// Determine new status
|
|
904
|
+
let newStatus = 'pending';
|
|
905
|
+
if (rejectedCount > 0) {
|
|
906
|
+
newStatus = 'rejected';
|
|
907
|
+
}
|
|
908
|
+
else if (approvedCount >= approverIds.length) {
|
|
909
|
+
newStatus = 'approved';
|
|
910
|
+
}
|
|
911
|
+
getDb().prepare(`
|
|
912
|
+
UPDATE approval_requests
|
|
913
|
+
SET approved_count = ?, rejected_count = ?, status = ?, completed_at = ?
|
|
914
|
+
WHERE request_id = ?
|
|
915
|
+
`).run(approvedCount, rejectedCount, newStatus, newStatus !== 'pending' ? new Date().toISOString() : null, request_id);
|
|
916
|
+
}
|
|
917
|
+
function getApprovalDecisions(request_id) {
|
|
918
|
+
return getDb().prepare(`
|
|
919
|
+
SELECT * FROM approval_decisions WHERE request_id = ? ORDER BY decided_at ASC
|
|
920
|
+
`).all(request_id);
|
|
921
|
+
}
|
|
922
|
+
// Phase 3: Escalation rule functions
|
|
923
|
+
function addEscalationRule(rule_id, name, escalation_targets, created_by, description, timeout_hours) {
|
|
924
|
+
getDb().prepare(`
|
|
925
|
+
INSERT OR REPLACE INTO escalation_rules (rule_id, name, description, timeout_hours, escalation_targets, created_by, status)
|
|
926
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
|
927
|
+
`).run(rule_id, name, description ?? null, timeout_hours ?? 24, JSON.stringify(escalation_targets), created_by);
|
|
928
|
+
}
|
|
929
|
+
function getEscalationRule(rule_id) {
|
|
930
|
+
return getDb().prepare(`
|
|
931
|
+
SELECT * FROM escalation_rules WHERE rule_id = ?
|
|
932
|
+
`).get(rule_id);
|
|
933
|
+
}
|
|
934
|
+
function getAllEscalationRules() {
|
|
935
|
+
return getDb().prepare(`
|
|
936
|
+
SELECT * FROM escalation_rules WHERE status = 'active' ORDER BY name ASC
|
|
937
|
+
`).all();
|
|
938
|
+
}
|
|
939
|
+
function updateEscalationRule(rule_id, updates) {
|
|
940
|
+
const current = getEscalationRule(rule_id);
|
|
941
|
+
if (!current)
|
|
942
|
+
throw new Error(`Rule ${rule_id} not found`);
|
|
943
|
+
getDb().prepare(`
|
|
944
|
+
UPDATE escalation_rules
|
|
945
|
+
SET name = ?, description = ?, timeout_hours = ?, escalation_targets = ?
|
|
946
|
+
WHERE rule_id = ?
|
|
947
|
+
`).run(updates.name ?? current.name, updates.description ?? current.description, updates.timeout_hours ?? current.timeout_hours, updates.escalation_targets ? JSON.stringify(updates.escalation_targets) : current.escalation_targets, rule_id);
|
|
948
|
+
}
|
|
949
|
+
function deleteEscalationRule(rule_id) {
|
|
950
|
+
getDb().prepare(`
|
|
951
|
+
UPDATE escalation_rules SET status = 'inactive' WHERE rule_id = ?
|
|
952
|
+
`).run(rule_id);
|
|
953
|
+
}
|
|
954
|
+
// Phase 3: Escalation request functions
|
|
955
|
+
function createEscalationRequest(request_id, approval_request_id, session_id, escalation_targets, deadline) {
|
|
956
|
+
getDb().prepare(`
|
|
957
|
+
INSERT INTO escalation_requests (request_id, approval_request_id, session_id, escalation_triggered_at, escalation_deadline, escalation_targets, status)
|
|
958
|
+
VALUES (?, ?, ?, datetime('now'), ?, ?, 'pending')
|
|
959
|
+
`).run(request_id, approval_request_id, session_id, deadline, JSON.stringify(escalation_targets));
|
|
960
|
+
}
|
|
961
|
+
function getEscalationRequest(request_id) {
|
|
962
|
+
return getDb().prepare(`
|
|
963
|
+
SELECT * FROM escalation_requests WHERE request_id = ?
|
|
964
|
+
`).get(request_id);
|
|
965
|
+
}
|
|
966
|
+
function getPendingEscalations() {
|
|
967
|
+
return getDb().prepare(`
|
|
968
|
+
SELECT * FROM escalation_requests WHERE status = 'pending' ORDER BY escalation_deadline ASC
|
|
969
|
+
`).all();
|
|
970
|
+
}
|
|
971
|
+
function getStaleApprovals(now) {
|
|
972
|
+
return getDb().prepare(`
|
|
973
|
+
SELECT DISTINCT ar.request_id as approval_request_id, ar.session_id
|
|
974
|
+
FROM approval_requests ar
|
|
975
|
+
WHERE ar.status = 'pending'
|
|
976
|
+
AND ar.triggered_at < ?
|
|
977
|
+
AND NOT EXISTS (
|
|
978
|
+
SELECT 1 FROM escalation_requests er
|
|
979
|
+
WHERE er.approval_request_id = ar.request_id
|
|
980
|
+
)
|
|
981
|
+
`).all(now);
|
|
982
|
+
}
|
|
983
|
+
function submitEscalationDecision(decision_id, escalation_request_id, target_id, decision, reason) {
|
|
984
|
+
getDb().prepare(`
|
|
985
|
+
INSERT INTO escalation_decisions (decision_id, escalation_request_id, target_id, decision, reason)
|
|
986
|
+
VALUES (?, ?, ?, ?, ?)
|
|
987
|
+
`).run(decision_id, escalation_request_id, target_id, decision, reason ?? null);
|
|
988
|
+
// Update escalation request status if all targets have decided
|
|
989
|
+
const request = getEscalationRequest(escalation_request_id);
|
|
990
|
+
if (!request)
|
|
991
|
+
return;
|
|
992
|
+
const decisions = getDb().prepare(`
|
|
993
|
+
SELECT DISTINCT decision FROM escalation_decisions WHERE escalation_request_id = ?
|
|
994
|
+
`).all(escalation_request_id);
|
|
995
|
+
const hasBlockDecision = decisions.some(d => d.decision === 'block');
|
|
996
|
+
const newStatus = hasBlockDecision ? 'blocked' : 'proceeded';
|
|
997
|
+
getDb().prepare(`
|
|
998
|
+
UPDATE escalation_requests
|
|
999
|
+
SET status = ?, decided_at = datetime('now'), decision = ?
|
|
1000
|
+
WHERE request_id = ?
|
|
1001
|
+
`).run(newStatus, newStatus, escalation_request_id);
|
|
1002
|
+
}
|
|
1003
|
+
function getEscalationDecisions(escalation_request_id) {
|
|
1004
|
+
return getDb().prepare(`
|
|
1005
|
+
SELECT * FROM escalation_decisions WHERE escalation_request_id = ? ORDER BY decided_at ASC
|
|
1006
|
+
`).all(escalation_request_id);
|
|
1007
|
+
}
|
|
1008
|
+
function getEscalationHistory(session_id) {
|
|
1009
|
+
return getDb().prepare(`
|
|
1010
|
+
SELECT * FROM escalation_requests WHERE session_id = ? ORDER BY escalation_triggered_at DESC
|
|
1011
|
+
`).all(session_id);
|
|
1012
|
+
}
|
|
444
1013
|
exports.default = db;
|