@shaifulshabuj-waymarks/server 0.1.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,218 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ require("dotenv/config");
40
+ const express_1 = __importDefault(require("express"));
41
+ const fs = __importStar(require("fs"));
42
+ const path = __importStar(require("path"));
43
+ const database_1 = require("../db/database");
44
+ const engine_1 = require("../policies/engine");
45
+ const handler_1 = require("../approvals/handler");
46
+ const app = (0, express_1.default)();
47
+ const PORT = 3001;
48
+ app.use(express_1.default.json());
49
+ app.use(express_1.default.urlencoded({ extended: true }));
50
+ // Serve UI — path works for both ts-node (src/api/) and compiled (dist/api/)
51
+ const UI_DIR = path.resolve(__dirname, '../../src/ui');
52
+ app.use(express_1.default.static(UI_DIR));
53
+ // GET /api/actions — list all actions (or ?count=true for pending count)
54
+ app.get('/api/actions', (req, res) => {
55
+ try {
56
+ if (req.query.count === 'true') {
57
+ return res.json({ count: (0, database_1.getPendingCount)() });
58
+ }
59
+ const actions = (0, database_1.getActions)();
60
+ res.json(actions);
61
+ }
62
+ catch (err) {
63
+ res.status(500).json({ error: err.message });
64
+ }
65
+ });
66
+ // POST /api/actions/:action_id/approve
67
+ app.post('/api/actions/:action_id/approve', async (req, res) => {
68
+ try {
69
+ const result = await (0, handler_1.approvePendingAction)(req.params.action_id, 'ui');
70
+ if (!result.success) {
71
+ const status = result.error === 'Action not found' ? 404 : 400;
72
+ return res.status(status).json({ error: result.error });
73
+ }
74
+ res.json(result);
75
+ }
76
+ catch (err) {
77
+ res.status(500).json({ error: err.message });
78
+ }
79
+ });
80
+ // POST /api/actions/:action_id/reject
81
+ app.post('/api/actions/:action_id/reject', async (req, res) => {
82
+ try {
83
+ const reason = req.body?.reason || 'Rejected';
84
+ const result = await (0, handler_1.rejectPendingAction)(req.params.action_id, reason);
85
+ if (!result.success) {
86
+ const status = result.error === 'Action not found' ? 404 : 400;
87
+ return res.status(status).json({ error: result.error });
88
+ }
89
+ res.json(result);
90
+ }
91
+ catch (err) {
92
+ res.status(500).json({ error: err.message });
93
+ }
94
+ });
95
+ // GET /api/actions/:action_id/status — lightweight status for agent polling
96
+ app.get('/api/actions/:action_id/status', (req, res) => {
97
+ try {
98
+ const action = (0, database_1.getAction)(req.params.action_id);
99
+ if (!action) {
100
+ return res.status(404).json({ error: 'Action not found' });
101
+ }
102
+ res.json({
103
+ status: action.status,
104
+ decision: action.decision,
105
+ approved_by: action.approved_by,
106
+ approved_at: action.approved_at,
107
+ rejected_reason: action.rejected_reason,
108
+ rejected_at: action.rejected_at,
109
+ });
110
+ }
111
+ catch (err) {
112
+ res.status(500).json({ error: err.message });
113
+ }
114
+ });
115
+ // GET /api/actions/:action_id — single action
116
+ app.get('/api/actions/:action_id', (req, res) => {
117
+ try {
118
+ const action = (0, database_1.getAction)(req.params.action_id);
119
+ if (!action) {
120
+ return res.status(404).json({ error: 'Action not found' });
121
+ }
122
+ res.json(action);
123
+ }
124
+ catch (err) {
125
+ res.status(500).json({ error: err.message });
126
+ }
127
+ });
128
+ // POST /api/actions/:action_id/rollback
129
+ app.post('/api/actions/:action_id/rollback', (req, res) => {
130
+ try {
131
+ const action = (0, database_1.getAction)(req.params.action_id);
132
+ if (!action) {
133
+ return res.status(404).json({ error: 'Action not found' });
134
+ }
135
+ if (action.tool_name !== 'write_file') {
136
+ return res.status(400).json({ error: 'Rollback only supported for write_file actions' });
137
+ }
138
+ if (action.rolled_back) {
139
+ return res.status(400).json({ error: 'Action already rolled back' });
140
+ }
141
+ if (!action.target_path) {
142
+ return res.status(400).json({ error: 'No target path on this action' });
143
+ }
144
+ // Bug 3: if no before_snapshot, file was newly created — delete it
145
+ if (!action.before_snapshot) {
146
+ fs.unlinkSync(action.target_path);
147
+ (0, database_1.markRolledBack)(action.action_id);
148
+ return res.json({ success: true, action: 'deleted', message: `Deleted new file: ${action.target_path}` });
149
+ }
150
+ // Restore file to before_snapshot
151
+ fs.mkdirSync(path.dirname(action.target_path), { recursive: true });
152
+ fs.writeFileSync(action.target_path, action.before_snapshot, 'utf8');
153
+ (0, database_1.markRolledBack)(action.action_id);
154
+ res.json({ success: true, action: 'restored', message: `Restored ${action.target_path} to previous state` });
155
+ }
156
+ catch (err) {
157
+ res.status(500).json({ error: err.message });
158
+ }
159
+ });
160
+ // POST /api/slack/interact — Slack interactive components (button clicks)
161
+ // For local development: use ngrok or similar to expose this endpoint publicly: ngrok http 3001
162
+ app.post('/api/slack/interact', async (req, res) => {
163
+ let payload;
164
+ try {
165
+ payload = JSON.parse(req.body.payload);
166
+ }
167
+ catch (err) {
168
+ console.error('Slack interact parse error:', err);
169
+ return res.status(400).json({ error: 'Invalid payload format' });
170
+ }
171
+ try {
172
+ if (!payload?.actions?.[0]) {
173
+ return res.status(400).json({ error: 'No actions in payload' });
174
+ }
175
+ const slackAction = payload.actions[0];
176
+ const actionId = slackAction.action_id;
177
+ const actionValue = slackAction.value; // waymark action_id
178
+ if (actionId === 'waymark_approve') {
179
+ const result = await (0, handler_1.approvePendingAction)(actionValue, 'slack');
180
+ return res.json({ text: result.success ? '✅ Approved by slack' : `❌ Error: ${result.error}` });
181
+ }
182
+ if (actionId === 'waymark_reject') {
183
+ const result = await (0, handler_1.rejectPendingAction)(actionValue, 'Rejected via Slack');
184
+ return res.json({ text: result.success ? '❌ Rejected by slack' : `❌ Error: ${result.error}` });
185
+ }
186
+ res.status(400).json({ error: `Unknown action_id: ${actionId}` });
187
+ }
188
+ catch (err) {
189
+ res.status(500).json({ error: err.message });
190
+ }
191
+ });
192
+ // GET /api/sessions
193
+ app.get('/api/sessions', (req, res) => {
194
+ try {
195
+ const sessions = (0, database_1.getSessions)();
196
+ res.json(sessions);
197
+ }
198
+ catch (err) {
199
+ res.status(500).json({ error: err.message });
200
+ }
201
+ });
202
+ // GET /api/config
203
+ app.get('/api/config', (req, res) => {
204
+ try {
205
+ res.json((0, engine_1.loadConfig)());
206
+ }
207
+ catch (err) {
208
+ res.status(500).json({ error: err.message });
209
+ }
210
+ });
211
+ // Fallback: serve UI for any unmatched route
212
+ app.get('*', (req, res) => {
213
+ res.sendFile(path.join(UI_DIR, 'index.html'));
214
+ });
215
+ app.listen(PORT, () => {
216
+ console.log(`Waymark UI + API running at http://localhost:${PORT}`);
217
+ });
218
+ exports.default = app;
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.approvePendingAction = approvePendingAction;
37
+ exports.rejectPendingAction = rejectPendingAction;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const database_1 = require("../db/database");
41
+ async function approvePendingAction(actionId, approvedBy = 'ui') {
42
+ const action = (0, database_1.getAction)(actionId);
43
+ if (!action) {
44
+ return { success: false, error: 'Action not found' };
45
+ }
46
+ if (action.status !== 'pending') {
47
+ return { success: false, error: `Action is not pending (current status: ${action.status})` };
48
+ }
49
+ let after_snapshot;
50
+ if (action.tool_name === 'write_file') {
51
+ const { path: filePath, content } = JSON.parse(action.input_payload);
52
+ const resolvedPath = path.resolve(filePath);
53
+ fs.mkdirSync(path.dirname(resolvedPath), { recursive: true });
54
+ fs.writeFileSync(resolvedPath, content, 'utf8');
55
+ after_snapshot = content;
56
+ }
57
+ else if (action.tool_name === 'read_file') {
58
+ // read_file: no re-execution needed, just mark approved
59
+ // (the file read already happened conceptually; approval means "yes, this was ok")
60
+ }
61
+ else {
62
+ return { success: false, error: `Unsupported tool for approval: ${action.tool_name}` };
63
+ }
64
+ (0, database_1.approveAction)(actionId, approvedBy, after_snapshot);
65
+ return { success: true, action: actionId };
66
+ }
67
+ async function rejectPendingAction(actionId, reason) {
68
+ const action = (0, database_1.getAction)(actionId);
69
+ if (!action) {
70
+ return { success: false, error: 'Action not found' };
71
+ }
72
+ if (action.status !== 'pending') {
73
+ return { success: false, error: `Action is not pending (current status: ${action.status})` };
74
+ }
75
+ (0, database_1.rejectAction)(actionId, reason);
76
+ return { success: true, action: actionId };
77
+ }
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.insertAction = insertAction;
40
+ exports.updateAction = updateAction;
41
+ exports.getActions = getActions;
42
+ exports.getAction = getAction;
43
+ exports.markRolledBack = markRolledBack;
44
+ exports.getSessions = getSessions;
45
+ exports.approveAction = approveAction;
46
+ exports.rejectAction = rejectAction;
47
+ exports.getPendingCount = getPendingCount;
48
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const PROJECT_ROOT = process.env.WAYMARK_PROJECT_ROOT || process.cwd();
52
+ const DATA_DIR = path.join(PROJECT_ROOT, 'data');
53
+ const DB_PATH = path.join(DATA_DIR, 'waymark.db');
54
+ // Ensure data directory exists
55
+ if (!fs.existsSync(DATA_DIR)) {
56
+ fs.mkdirSync(DATA_DIR, { recursive: true });
57
+ }
58
+ const db = new better_sqlite3_1.default(DB_PATH);
59
+ // Create table on startup
60
+ db.exec(`
61
+ CREATE TABLE IF NOT EXISTS action_log (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ action_id TEXT UNIQUE NOT NULL,
64
+ session_id TEXT NOT NULL,
65
+ tool_name TEXT NOT NULL,
66
+ target_path TEXT,
67
+ input_payload TEXT NOT NULL,
68
+ before_snapshot TEXT,
69
+ after_snapshot TEXT,
70
+ status TEXT NOT NULL DEFAULT 'pending',
71
+ error_message TEXT,
72
+ stdout TEXT,
73
+ stderr TEXT,
74
+ rolled_back INTEGER NOT NULL DEFAULT 0,
75
+ rolled_back_at TEXT,
76
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
77
+ )
78
+ `);
79
+ // Migrate existing DBs — add stdout/stderr if not present
80
+ try {
81
+ db.exec('ALTER TABLE action_log ADD COLUMN stdout TEXT');
82
+ }
83
+ catch { }
84
+ try {
85
+ db.exec('ALTER TABLE action_log ADD COLUMN stderr TEXT');
86
+ }
87
+ catch { }
88
+ // Migrate v2: policy engine columns
89
+ try {
90
+ db.exec("ALTER TABLE action_log ADD COLUMN decision TEXT NOT NULL DEFAULT 'allow'");
91
+ }
92
+ catch { }
93
+ try {
94
+ db.exec('ALTER TABLE action_log ADD COLUMN policy_reason TEXT');
95
+ }
96
+ catch { }
97
+ try {
98
+ db.exec('ALTER TABLE action_log ADD COLUMN matched_rule TEXT');
99
+ }
100
+ catch { }
101
+ // Migrate v3: approval flow columns
102
+ try {
103
+ db.exec('ALTER TABLE action_log ADD COLUMN approved_at TEXT');
104
+ }
105
+ catch { }
106
+ try {
107
+ db.exec('ALTER TABLE action_log ADD COLUMN approved_by TEXT');
108
+ }
109
+ catch { }
110
+ try {
111
+ db.exec('ALTER TABLE action_log ADD COLUMN rejected_at TEXT');
112
+ }
113
+ catch { }
114
+ try {
115
+ db.exec('ALTER TABLE action_log ADD COLUMN rejected_reason TEXT');
116
+ }
117
+ catch { }
118
+ const insertStmt = db.prepare(`
119
+ INSERT INTO action_log (action_id, session_id, tool_name, target_path, input_payload, before_snapshot, status, decision, policy_reason, matched_rule)
120
+ VALUES (@action_id, @session_id, @tool_name, @target_path, @input_payload, @before_snapshot, @status, @decision, @policy_reason, @matched_rule)
121
+ `);
122
+ const updateStmt = db.prepare(`
123
+ UPDATE action_log
124
+ SET status = COALESCE(@status, status),
125
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot),
126
+ error_message = COALESCE(@error_message, error_message),
127
+ stdout = COALESCE(@stdout, stdout),
128
+ stderr = COALESCE(@stderr, stderr)
129
+ WHERE action_id = @action_id
130
+ `);
131
+ function insertAction(params) {
132
+ insertStmt.run({
133
+ action_id: params.action_id,
134
+ session_id: params.session_id,
135
+ tool_name: params.tool_name,
136
+ target_path: params.target_path ?? null,
137
+ input_payload: params.input_payload,
138
+ before_snapshot: params.before_snapshot ?? null,
139
+ status: params.status,
140
+ decision: params.decision ?? 'allow',
141
+ policy_reason: params.policy_reason ?? null,
142
+ matched_rule: params.matched_rule ?? null,
143
+ });
144
+ }
145
+ function updateAction(action_id, params) {
146
+ updateStmt.run({
147
+ action_id,
148
+ status: params.status ?? null,
149
+ after_snapshot: params.after_snapshot ?? null,
150
+ error_message: params.error_message ?? null,
151
+ stdout: params.stdout ?? null,
152
+ stderr: params.stderr ?? null,
153
+ });
154
+ }
155
+ function getActions() {
156
+ return db.prepare(`
157
+ SELECT * FROM action_log ORDER BY created_at DESC LIMIT 100
158
+ `).all();
159
+ }
160
+ function getAction(action_id) {
161
+ return db.prepare(`
162
+ SELECT * FROM action_log WHERE action_id = ?
163
+ `).get(action_id);
164
+ }
165
+ function markRolledBack(action_id) {
166
+ db.prepare(`
167
+ UPDATE action_log
168
+ SET rolled_back = 1, rolled_back_at = datetime('now')
169
+ WHERE action_id = ?
170
+ `).run(action_id);
171
+ }
172
+ function getSessions() {
173
+ return db.prepare(`
174
+ SELECT session_id,
175
+ COUNT(*) as action_count,
176
+ MAX(created_at) as latest
177
+ FROM action_log
178
+ GROUP BY session_id
179
+ ORDER BY latest DESC
180
+ `).all();
181
+ }
182
+ function approveAction(action_id, approved_by, after_snapshot) {
183
+ db.prepare(`
184
+ UPDATE action_log
185
+ SET status = 'success',
186
+ decision = 'allow',
187
+ approved_at = datetime('now'),
188
+ approved_by = @approved_by,
189
+ after_snapshot = COALESCE(@after_snapshot, after_snapshot)
190
+ WHERE action_id = @action_id
191
+ `).run({ action_id, approved_by, after_snapshot: after_snapshot ?? null });
192
+ }
193
+ function rejectAction(action_id, reason) {
194
+ db.prepare(`
195
+ UPDATE action_log
196
+ SET status = 'rejected',
197
+ decision = 'rejected',
198
+ rejected_at = datetime('now'),
199
+ rejected_reason = @reason
200
+ WHERE action_id = @action_id
201
+ `).run({ action_id, reason });
202
+ }
203
+ function getPendingCount() {
204
+ const row = db.prepare(`
205
+ SELECT COUNT(*) as count FROM action_log WHERE status = 'pending'
206
+ `).get();
207
+ return row.count;
208
+ }
209
+ exports.default = db;