@way_marks/server 1.0.0 → 2.0.1
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/db/database.js +459 -433
- package/dist/policies/engine.js +11 -5
- package/dist/risk/analyzer.js +22 -11
- package/package.json +1 -1
- package/dist/approval/manager.test.js +0 -315
- package/dist/approvals/handler.test.js +0 -172
- package/dist/escalation/manager.test.js +0 -519
- package/dist/policies/engine.test.js +0 -241
- package/dist/risk/analyzer.test.js +0 -482
- package/dist/rollback/manager.test.js +0 -552
package/dist/db/database.js
CHANGED
|
@@ -90,377 +90,403 @@ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
|
|
|
90
90
|
const DB_PATH = process.env.WAYMARK_DB_PATH
|
|
91
91
|
|| path.join(PROJECT_ROOT, '.waymark', 'waymark.db');
|
|
92
92
|
const DB_DIR = path.dirname(DB_PATH);
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
+
}
|
|
462
488
|
function insertAction(params) {
|
|
463
|
-
|
|
489
|
+
getInsertStmt().run({
|
|
464
490
|
action_id: params.action_id,
|
|
465
491
|
session_id: params.session_id,
|
|
466
492
|
tool_name: params.tool_name,
|
|
@@ -478,7 +504,7 @@ function insertAction(params) {
|
|
|
478
504
|
});
|
|
479
505
|
}
|
|
480
506
|
function updateAction(action_id, params) {
|
|
481
|
-
|
|
507
|
+
getUpdateStmt().run({
|
|
482
508
|
action_id,
|
|
483
509
|
status: params.status ?? null,
|
|
484
510
|
after_snapshot: params.after_snapshot ?? null,
|
|
@@ -488,24 +514,24 @@ function updateAction(action_id, params) {
|
|
|
488
514
|
});
|
|
489
515
|
}
|
|
490
516
|
function getActions() {
|
|
491
|
-
return
|
|
517
|
+
return getDb().prepare(`
|
|
492
518
|
SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
|
|
493
519
|
`).all();
|
|
494
520
|
}
|
|
495
521
|
function getAction(action_id) {
|
|
496
|
-
return
|
|
522
|
+
return getDb().prepare(`
|
|
497
523
|
SELECT * FROM action_log WHERE action_id = ?
|
|
498
524
|
`).get(action_id);
|
|
499
525
|
}
|
|
500
526
|
function markRolledBack(action_id) {
|
|
501
|
-
|
|
527
|
+
getDb().prepare(`
|
|
502
528
|
UPDATE action_log
|
|
503
529
|
SET rolled_back = 1, rolled_back_at = datetime('now')
|
|
504
530
|
WHERE action_id = ?
|
|
505
531
|
`).run(action_id);
|
|
506
532
|
}
|
|
507
533
|
function getSessions() {
|
|
508
|
-
return
|
|
534
|
+
return getDb().prepare(`
|
|
509
535
|
SELECT session_id,
|
|
510
536
|
COUNT(*) as action_count,
|
|
511
537
|
MAX(created_at) as latest
|
|
@@ -515,7 +541,7 @@ function getSessions() {
|
|
|
515
541
|
`).all();
|
|
516
542
|
}
|
|
517
543
|
function approveAction(action_id, approved_by, after_snapshot) {
|
|
518
|
-
|
|
544
|
+
getDb().prepare(`
|
|
519
545
|
UPDATE action_log
|
|
520
546
|
SET status = 'success',
|
|
521
547
|
decision = 'allow',
|
|
@@ -526,7 +552,7 @@ function approveAction(action_id, approved_by, after_snapshot) {
|
|
|
526
552
|
`).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
|
|
527
553
|
}
|
|
528
554
|
function rejectAction(action_id, reason) {
|
|
529
|
-
|
|
555
|
+
getDb().prepare(`
|
|
530
556
|
UPDATE action_log
|
|
531
557
|
SET status = 'rejected',
|
|
532
558
|
decision = 'rejected',
|
|
@@ -536,7 +562,7 @@ function rejectAction(action_id, reason) {
|
|
|
536
562
|
`).run({ action_id, reason });
|
|
537
563
|
}
|
|
538
564
|
function getPendingCount() {
|
|
539
|
-
const row =
|
|
565
|
+
const row = getDb().prepare(`
|
|
540
566
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
|
|
541
567
|
`).get();
|
|
542
568
|
return row.count;
|
|
@@ -560,12 +586,12 @@ function getActionsWithFiltering(filter = {}) {
|
|
|
560
586
|
params.search = `%${filter.search}%`;
|
|
561
587
|
}
|
|
562
588
|
// Get total count
|
|
563
|
-
const countRow =
|
|
589
|
+
const countRow = getDb().prepare(`
|
|
564
590
|
SELECT COUNT(*) as count FROM action_log WHERE ${where}
|
|
565
591
|
`).get(params);
|
|
566
592
|
const totalCount = countRow.count;
|
|
567
593
|
// Get paginated results
|
|
568
|
-
const actions =
|
|
594
|
+
const actions = getDb().prepare(`
|
|
569
595
|
SELECT * FROM action_log WHERE ${where}
|
|
570
596
|
ORDER BY created_at DESC
|
|
571
597
|
LIMIT @limit OFFSET @offset
|
|
@@ -583,12 +609,12 @@ function archiveOldActions(daysOld = 30) {
|
|
|
583
609
|
cutoffDate.setDate(cutoffDate.getDate() - daysOld);
|
|
584
610
|
const cutoffStr = cutoffDate.toISOString();
|
|
585
611
|
// Copy old actions to archive
|
|
586
|
-
const result =
|
|
612
|
+
const result = getDb().prepare(`
|
|
587
613
|
INSERT OR IGNORE INTO action_archive
|
|
588
614
|
SELECT * FROM action_log WHERE created_at < @cutoff
|
|
589
615
|
`).run({ cutoff: cutoffStr });
|
|
590
616
|
// Delete from main table (but keep recent entries)
|
|
591
|
-
const deleteStmt =
|
|
617
|
+
const deleteStmt = getDb().prepare(`
|
|
592
618
|
DELETE FROM action_log WHERE created_at < @cutoff
|
|
593
619
|
AND id NOT IN (
|
|
594
620
|
SELECT id FROM action_log ORDER BY created_at DESC LIMIT 1000
|
|
@@ -615,11 +641,11 @@ function getArchivedActionsWithFiltering(filter = {}) {
|
|
|
615
641
|
where += ' AND (action_id LIKE @search OR target_path LIKE @search)';
|
|
616
642
|
params.search = `%${filter.search}%`;
|
|
617
643
|
}
|
|
618
|
-
const countRow =
|
|
644
|
+
const countRow = getDb().prepare(`
|
|
619
645
|
SELECT COUNT(*) as count FROM action_archive WHERE ${where}
|
|
620
646
|
`).get(params);
|
|
621
647
|
const totalCount = countRow.count;
|
|
622
|
-
const actions =
|
|
648
|
+
const actions = getDb().prepare(`
|
|
623
649
|
SELECT * FROM action_archive WHERE ${where}
|
|
624
650
|
ORDER BY created_at DESC
|
|
625
651
|
LIMIT @limit OFFSET @offset
|
|
@@ -637,34 +663,34 @@ function getSummaryStats() {
|
|
|
637
663
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate()).toISOString();
|
|
638
664
|
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
|
|
639
665
|
const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
640
|
-
const total =
|
|
666
|
+
const total = getDb().prepare(`
|
|
641
667
|
SELECT COUNT(*) as count FROM action_log
|
|
642
668
|
`).get();
|
|
643
|
-
const pending =
|
|
669
|
+
const pending = getDb().prepare(`
|
|
644
670
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
|
|
645
671
|
`).get();
|
|
646
|
-
const approved =
|
|
672
|
+
const approved = getDb().prepare(`
|
|
647
673
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'success' OR decision = 'allow'
|
|
648
674
|
`).get();
|
|
649
|
-
const rejected =
|
|
675
|
+
const rejected = getDb().prepare(`
|
|
650
676
|
SELECT COUNT(*) as count FROM action_log WHERE status = 'rejected' OR decision = 'rejected'
|
|
651
677
|
`).get();
|
|
652
|
-
const todayCount =
|
|
678
|
+
const todayCount = getDb().prepare(`
|
|
653
679
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @today
|
|
654
680
|
`).get({ today });
|
|
655
|
-
const weekCount =
|
|
681
|
+
const weekCount = getDb().prepare(`
|
|
656
682
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @weekAgo
|
|
657
683
|
`).get({ weekAgo });
|
|
658
|
-
const monthCount =
|
|
684
|
+
const monthCount = getDb().prepare(`
|
|
659
685
|
SELECT COUNT(*) as count FROM action_log WHERE created_at >= @monthAgo
|
|
660
686
|
`).get({ monthAgo });
|
|
661
|
-
const topTools =
|
|
687
|
+
const topTools = getDb().prepare(`
|
|
662
688
|
SELECT tool_name as tool, COUNT(*) as count FROM action_log
|
|
663
689
|
GROUP BY tool_name
|
|
664
690
|
ORDER BY count DESC
|
|
665
691
|
LIMIT 5
|
|
666
692
|
`).all();
|
|
667
|
-
const topPaths =
|
|
693
|
+
const topPaths = getDb().prepare(`
|
|
668
694
|
SELECT target_path as path, COUNT(*) as count FROM action_log
|
|
669
695
|
WHERE target_path IS NOT NULL
|
|
670
696
|
GROUP BY target_path
|
|
@@ -685,23 +711,23 @@ function getSummaryStats() {
|
|
|
685
711
|
}
|
|
686
712
|
// Phase 1: Session management functions
|
|
687
713
|
function createSession(session_id, user_id, project_id) {
|
|
688
|
-
|
|
714
|
+
getDb().prepare(`
|
|
689
715
|
INSERT OR IGNORE INTO sessions (session_id, user_id, project_id, status)
|
|
690
716
|
VALUES (?, ?, ?, 'active')
|
|
691
717
|
`).run(session_id, user_id ?? null, project_id ?? null);
|
|
692
718
|
}
|
|
693
719
|
function getSession(session_id) {
|
|
694
|
-
return
|
|
720
|
+
return getDb().prepare(`
|
|
695
721
|
SELECT * FROM sessions WHERE session_id = ?
|
|
696
722
|
`).get(session_id);
|
|
697
723
|
}
|
|
698
724
|
function getAllSessions() {
|
|
699
|
-
return
|
|
725
|
+
return getDb().prepare(`
|
|
700
726
|
SELECT * FROM sessions ORDER BY created_at DESC
|
|
701
727
|
`).all();
|
|
702
728
|
}
|
|
703
729
|
function getSessionActions(session_id) {
|
|
704
|
-
return
|
|
730
|
+
return getDb().prepare(`
|
|
705
731
|
SELECT * FROM action_log
|
|
706
732
|
WHERE session_id = ?
|
|
707
733
|
ORDER BY created_at ASC
|
|
@@ -710,7 +736,7 @@ function getSessionActions(session_id) {
|
|
|
710
736
|
function rollbackSession(session_id) {
|
|
711
737
|
try {
|
|
712
738
|
// Start transaction
|
|
713
|
-
const transaction =
|
|
739
|
+
const transaction = getDb().transaction(() => {
|
|
714
740
|
// Get all actions in session
|
|
715
741
|
const actions = getSessionActions(session_id);
|
|
716
742
|
if (actions.length === 0) {
|
|
@@ -740,7 +766,7 @@ function rollbackSession(session_id) {
|
|
|
740
766
|
}
|
|
741
767
|
}
|
|
742
768
|
// Mark session as rolled back
|
|
743
|
-
|
|
769
|
+
getDb().prepare(`
|
|
744
770
|
UPDATE sessions
|
|
745
771
|
SET rolled_back_at = datetime('now'), status = 'rolled_back'
|
|
746
772
|
WHERE session_id = ?
|
|
@@ -764,7 +790,7 @@ function rollbackSession(session_id) {
|
|
|
764
790
|
}
|
|
765
791
|
}
|
|
766
792
|
function markSessionRolledBack(session_id) {
|
|
767
|
-
|
|
793
|
+
getDb().prepare(`
|
|
768
794
|
UPDATE sessions
|
|
769
795
|
SET rolled_back_at = datetime('now'), status = 'rolled_back'
|
|
770
796
|
WHERE session_id = ?
|
|
@@ -772,45 +798,45 @@ function markSessionRolledBack(session_id) {
|
|
|
772
798
|
}
|
|
773
799
|
// Phase 2: Team member functions
|
|
774
800
|
function addTeamMember(member_id, name, email, added_by, slack_id) {
|
|
775
|
-
|
|
801
|
+
getDb().prepare(`
|
|
776
802
|
INSERT OR REPLACE INTO team_members (member_id, name, email, slack_id, added_by, status)
|
|
777
803
|
VALUES (?, ?, ?, ?, ?, 'active')
|
|
778
804
|
`).run(member_id, name, email, slack_id ?? null, added_by);
|
|
779
805
|
}
|
|
780
806
|
function getTeamMember(member_id) {
|
|
781
|
-
return
|
|
807
|
+
return getDb().prepare(`
|
|
782
808
|
SELECT * FROM team_members WHERE member_id = ?
|
|
783
809
|
`).get(member_id);
|
|
784
810
|
}
|
|
785
811
|
function getTeamMemberByEmail(email) {
|
|
786
|
-
return
|
|
812
|
+
return getDb().prepare(`
|
|
787
813
|
SELECT * FROM team_members WHERE email = ?
|
|
788
814
|
`).get(email);
|
|
789
815
|
}
|
|
790
816
|
function getAllTeamMembers() {
|
|
791
|
-
return
|
|
817
|
+
return getDb().prepare(`
|
|
792
818
|
SELECT * FROM team_members WHERE status = 'active' ORDER BY name ASC
|
|
793
819
|
`).all();
|
|
794
820
|
}
|
|
795
821
|
function removeTeamMember(member_id) {
|
|
796
|
-
|
|
822
|
+
getDb().prepare(`
|
|
797
823
|
UPDATE team_members SET status = 'inactive' WHERE member_id = ?
|
|
798
824
|
`).run(member_id);
|
|
799
825
|
}
|
|
800
826
|
// Phase 2: Approval route functions
|
|
801
827
|
function addApprovalRoute(route_id, name, approver_ids, created_by, description, condition_type, condition_json) {
|
|
802
|
-
|
|
828
|
+
getDb().prepare(`
|
|
803
829
|
INSERT OR REPLACE INTO approval_routes (route_id, name, description, condition_type, condition_json, approver_ids, created_by, status)
|
|
804
830
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'active')
|
|
805
831
|
`).run(route_id, name, description ?? null, condition_type ?? 'all_sessions', condition_json ?? null, JSON.stringify(approver_ids), created_by);
|
|
806
832
|
}
|
|
807
833
|
function getApprovalRoute(route_id) {
|
|
808
|
-
return
|
|
834
|
+
return getDb().prepare(`
|
|
809
835
|
SELECT * FROM approval_routes WHERE route_id = ?
|
|
810
836
|
`).get(route_id);
|
|
811
837
|
}
|
|
812
838
|
function getAllApprovalRoutes() {
|
|
813
|
-
return
|
|
839
|
+
return getDb().prepare(`
|
|
814
840
|
SELECT * FROM approval_routes WHERE status = 'active' ORDER BY name ASC
|
|
815
841
|
`).all();
|
|
816
842
|
}
|
|
@@ -818,49 +844,49 @@ function updateApprovalRoute(route_id, updates) {
|
|
|
818
844
|
const current = getApprovalRoute(route_id);
|
|
819
845
|
if (!current)
|
|
820
846
|
throw new Error(`Route ${route_id} not found`);
|
|
821
|
-
|
|
847
|
+
getDb().prepare(`
|
|
822
848
|
UPDATE approval_routes
|
|
823
849
|
SET name = ?, description = ?, approver_ids = ?
|
|
824
850
|
WHERE route_id = ?
|
|
825
851
|
`).run(updates.name ?? current.name, updates.description ?? current.description, updates.approver_ids ? JSON.stringify(updates.approver_ids) : current.approver_ids, route_id);
|
|
826
852
|
}
|
|
827
853
|
function deleteApprovalRoute(route_id) {
|
|
828
|
-
|
|
854
|
+
getDb().prepare(`
|
|
829
855
|
UPDATE approval_routes SET status = 'inactive' WHERE route_id = ?
|
|
830
856
|
`).run(route_id);
|
|
831
857
|
}
|
|
832
858
|
// Phase 2: Approval request functions
|
|
833
859
|
function createApprovalRequest(request_id, session_id, route_id, triggered_by, approver_ids) {
|
|
834
|
-
|
|
860
|
+
getDb().prepare(`
|
|
835
861
|
INSERT INTO approval_requests (request_id, session_id, route_id, triggered_by, approver_ids, status)
|
|
836
862
|
VALUES (?, ?, ?, ?, ?, 'pending')
|
|
837
863
|
`).run(request_id, session_id, route_id, triggered_by, JSON.stringify(approver_ids));
|
|
838
864
|
}
|
|
839
865
|
function getApprovalRequest(request_id) {
|
|
840
|
-
return
|
|
866
|
+
return getDb().prepare(`
|
|
841
867
|
SELECT * FROM approval_requests WHERE request_id = ?
|
|
842
868
|
`).get(request_id);
|
|
843
869
|
}
|
|
844
870
|
function getSessionApprovalRequests(session_id) {
|
|
845
|
-
return
|
|
871
|
+
return getDb().prepare(`
|
|
846
872
|
SELECT * FROM approval_requests WHERE session_id = ? ORDER BY triggered_at DESC
|
|
847
873
|
`).all(session_id);
|
|
848
874
|
}
|
|
849
875
|
function getPendingApprovals(approver_id) {
|
|
850
876
|
if (approver_id) {
|
|
851
|
-
return
|
|
877
|
+
return getDb().prepare(`
|
|
852
878
|
SELECT * FROM approval_requests
|
|
853
879
|
WHERE status = 'pending' AND approver_ids LIKE ?
|
|
854
880
|
ORDER BY triggered_at DESC
|
|
855
881
|
`).all(`%"${approver_id}"%`);
|
|
856
882
|
}
|
|
857
|
-
return
|
|
883
|
+
return getDb().prepare(`
|
|
858
884
|
SELECT * FROM approval_requests WHERE status = 'pending' ORDER BY triggered_at DESC
|
|
859
885
|
`).all();
|
|
860
886
|
}
|
|
861
887
|
function submitApprovalDecision(decision_id, request_id, approver_id, decision, reason) {
|
|
862
888
|
// Insert decision
|
|
863
|
-
|
|
889
|
+
getDb().prepare(`
|
|
864
890
|
INSERT INTO approval_decisions (decision_id, request_id, approver_id, decision, reason)
|
|
865
891
|
VALUES (?, ?, ?, ?, ?)
|
|
866
892
|
`).run(decision_id, request_id, approver_id, decision, reason ?? null);
|
|
@@ -868,7 +894,7 @@ function submitApprovalDecision(decision_id, request_id, approver_id, decision,
|
|
|
868
894
|
const request = getApprovalRequest(request_id);
|
|
869
895
|
if (!request)
|
|
870
896
|
throw new Error(`Request ${request_id} not found`);
|
|
871
|
-
const decisions =
|
|
897
|
+
const decisions = getDb().prepare(`
|
|
872
898
|
SELECT decision FROM approval_decisions WHERE request_id = ?
|
|
873
899
|
`).all(request_id);
|
|
874
900
|
const approvedCount = decisions.filter(d => d.decision === 'approve').length;
|
|
@@ -882,31 +908,31 @@ function submitApprovalDecision(decision_id, request_id, approver_id, decision,
|
|
|
882
908
|
else if (approvedCount >= approverIds.length) {
|
|
883
909
|
newStatus = 'approved';
|
|
884
910
|
}
|
|
885
|
-
|
|
911
|
+
getDb().prepare(`
|
|
886
912
|
UPDATE approval_requests
|
|
887
913
|
SET approved_count = ?, rejected_count = ?, status = ?, completed_at = ?
|
|
888
914
|
WHERE request_id = ?
|
|
889
915
|
`).run(approvedCount, rejectedCount, newStatus, newStatus !== 'pending' ? new Date().toISOString() : null, request_id);
|
|
890
916
|
}
|
|
891
917
|
function getApprovalDecisions(request_id) {
|
|
892
|
-
return
|
|
918
|
+
return getDb().prepare(`
|
|
893
919
|
SELECT * FROM approval_decisions WHERE request_id = ? ORDER BY decided_at ASC
|
|
894
920
|
`).all(request_id);
|
|
895
921
|
}
|
|
896
922
|
// Phase 3: Escalation rule functions
|
|
897
923
|
function addEscalationRule(rule_id, name, escalation_targets, created_by, description, timeout_hours) {
|
|
898
|
-
|
|
924
|
+
getDb().prepare(`
|
|
899
925
|
INSERT OR REPLACE INTO escalation_rules (rule_id, name, description, timeout_hours, escalation_targets, created_by, status)
|
|
900
926
|
VALUES (?, ?, ?, ?, ?, ?, 'active')
|
|
901
927
|
`).run(rule_id, name, description ?? null, timeout_hours ?? 24, JSON.stringify(escalation_targets), created_by);
|
|
902
928
|
}
|
|
903
929
|
function getEscalationRule(rule_id) {
|
|
904
|
-
return
|
|
930
|
+
return getDb().prepare(`
|
|
905
931
|
SELECT * FROM escalation_rules WHERE rule_id = ?
|
|
906
932
|
`).get(rule_id);
|
|
907
933
|
}
|
|
908
934
|
function getAllEscalationRules() {
|
|
909
|
-
return
|
|
935
|
+
return getDb().prepare(`
|
|
910
936
|
SELECT * FROM escalation_rules WHERE status = 'active' ORDER BY name ASC
|
|
911
937
|
`).all();
|
|
912
938
|
}
|
|
@@ -914,36 +940,36 @@ function updateEscalationRule(rule_id, updates) {
|
|
|
914
940
|
const current = getEscalationRule(rule_id);
|
|
915
941
|
if (!current)
|
|
916
942
|
throw new Error(`Rule ${rule_id} not found`);
|
|
917
|
-
|
|
943
|
+
getDb().prepare(`
|
|
918
944
|
UPDATE escalation_rules
|
|
919
945
|
SET name = ?, description = ?, timeout_hours = ?, escalation_targets = ?
|
|
920
946
|
WHERE rule_id = ?
|
|
921
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);
|
|
922
948
|
}
|
|
923
949
|
function deleteEscalationRule(rule_id) {
|
|
924
|
-
|
|
950
|
+
getDb().prepare(`
|
|
925
951
|
UPDATE escalation_rules SET status = 'inactive' WHERE rule_id = ?
|
|
926
952
|
`).run(rule_id);
|
|
927
953
|
}
|
|
928
954
|
// Phase 3: Escalation request functions
|
|
929
955
|
function createEscalationRequest(request_id, approval_request_id, session_id, escalation_targets, deadline) {
|
|
930
|
-
|
|
956
|
+
getDb().prepare(`
|
|
931
957
|
INSERT INTO escalation_requests (request_id, approval_request_id, session_id, escalation_triggered_at, escalation_deadline, escalation_targets, status)
|
|
932
958
|
VALUES (?, ?, ?, datetime('now'), ?, ?, 'pending')
|
|
933
959
|
`).run(request_id, approval_request_id, session_id, deadline, JSON.stringify(escalation_targets));
|
|
934
960
|
}
|
|
935
961
|
function getEscalationRequest(request_id) {
|
|
936
|
-
return
|
|
962
|
+
return getDb().prepare(`
|
|
937
963
|
SELECT * FROM escalation_requests WHERE request_id = ?
|
|
938
964
|
`).get(request_id);
|
|
939
965
|
}
|
|
940
966
|
function getPendingEscalations() {
|
|
941
|
-
return
|
|
967
|
+
return getDb().prepare(`
|
|
942
968
|
SELECT * FROM escalation_requests WHERE status = 'pending' ORDER BY escalation_deadline ASC
|
|
943
969
|
`).all();
|
|
944
970
|
}
|
|
945
971
|
function getStaleApprovals(now) {
|
|
946
|
-
return
|
|
972
|
+
return getDb().prepare(`
|
|
947
973
|
SELECT DISTINCT ar.request_id as approval_request_id, ar.session_id
|
|
948
974
|
FROM approval_requests ar
|
|
949
975
|
WHERE ar.status = 'pending'
|
|
@@ -955,7 +981,7 @@ function getStaleApprovals(now) {
|
|
|
955
981
|
`).all(now);
|
|
956
982
|
}
|
|
957
983
|
function submitEscalationDecision(decision_id, escalation_request_id, target_id, decision, reason) {
|
|
958
|
-
|
|
984
|
+
getDb().prepare(`
|
|
959
985
|
INSERT INTO escalation_decisions (decision_id, escalation_request_id, target_id, decision, reason)
|
|
960
986
|
VALUES (?, ?, ?, ?, ?)
|
|
961
987
|
`).run(decision_id, escalation_request_id, target_id, decision, reason ?? null);
|
|
@@ -963,24 +989,24 @@ function submitEscalationDecision(decision_id, escalation_request_id, target_id,
|
|
|
963
989
|
const request = getEscalationRequest(escalation_request_id);
|
|
964
990
|
if (!request)
|
|
965
991
|
return;
|
|
966
|
-
const decisions =
|
|
992
|
+
const decisions = getDb().prepare(`
|
|
967
993
|
SELECT DISTINCT decision FROM escalation_decisions WHERE escalation_request_id = ?
|
|
968
994
|
`).all(escalation_request_id);
|
|
969
995
|
const hasBlockDecision = decisions.some(d => d.decision === 'block');
|
|
970
996
|
const newStatus = hasBlockDecision ? 'blocked' : 'proceeded';
|
|
971
|
-
|
|
997
|
+
getDb().prepare(`
|
|
972
998
|
UPDATE escalation_requests
|
|
973
999
|
SET status = ?, decided_at = datetime('now'), decision = ?
|
|
974
1000
|
WHERE request_id = ?
|
|
975
1001
|
`).run(newStatus, newStatus, escalation_request_id);
|
|
976
1002
|
}
|
|
977
1003
|
function getEscalationDecisions(escalation_request_id) {
|
|
978
|
-
return
|
|
1004
|
+
return getDb().prepare(`
|
|
979
1005
|
SELECT * FROM escalation_decisions WHERE escalation_request_id = ? ORDER BY decided_at ASC
|
|
980
1006
|
`).all(escalation_request_id);
|
|
981
1007
|
}
|
|
982
1008
|
function getEscalationHistory(session_id) {
|
|
983
|
-
return
|
|
1009
|
+
return getDb().prepare(`
|
|
984
1010
|
SELECT * FROM escalation_requests WHERE session_id = ? ORDER BY escalation_triggered_at DESC
|
|
985
1011
|
`).all(session_id);
|
|
986
1012
|
}
|