claude-remote-guard 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +433 -0
- package/dist/bin/cli.d.ts +3 -0
- package/dist/bin/cli.d.ts.map +1 -0
- package/dist/bin/cli.js +427 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/hook.d.ts +3 -0
- package/dist/bin/hook.d.ts.map +1 -0
- package/dist/bin/hook.js +136 -0
- package/dist/bin/hook.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/claude-settings.d.ts +11 -0
- package/dist/lib/claude-settings.d.ts.map +1 -0
- package/dist/lib/claude-settings.js +96 -0
- package/dist/lib/claude-settings.js.map +1 -0
- package/dist/lib/config.d.ts +47 -0
- package/dist/lib/config.d.ts.map +1 -0
- package/dist/lib/config.js +177 -0
- package/dist/lib/config.js.map +1 -0
- package/dist/lib/edge-function.d.ts +14 -0
- package/dist/lib/edge-function.d.ts.map +1 -0
- package/dist/lib/edge-function.js +521 -0
- package/dist/lib/edge-function.js.map +1 -0
- package/dist/lib/firebase.d.ts +27 -0
- package/dist/lib/firebase.d.ts.map +1 -0
- package/dist/lib/firebase.js +136 -0
- package/dist/lib/firebase.js.map +1 -0
- package/dist/lib/messenger/base.d.ts +6 -0
- package/dist/lib/messenger/base.d.ts.map +1 -0
- package/dist/lib/messenger/base.js +34 -0
- package/dist/lib/messenger/base.js.map +1 -0
- package/dist/lib/messenger/factory.d.ts +15 -0
- package/dist/lib/messenger/factory.d.ts.map +1 -0
- package/dist/lib/messenger/factory.js +37 -0
- package/dist/lib/messenger/factory.js.map +1 -0
- package/dist/lib/messenger/index.d.ts +7 -0
- package/dist/lib/messenger/index.d.ts.map +1 -0
- package/dist/lib/messenger/index.js +9 -0
- package/dist/lib/messenger/index.js.map +1 -0
- package/dist/lib/messenger/slack.d.ts +14 -0
- package/dist/lib/messenger/slack.d.ts.map +1 -0
- package/dist/lib/messenger/slack.js +169 -0
- package/dist/lib/messenger/slack.js.map +1 -0
- package/dist/lib/messenger/telegram.d.ts +15 -0
- package/dist/lib/messenger/telegram.d.ts.map +1 -0
- package/dist/lib/messenger/telegram.js +120 -0
- package/dist/lib/messenger/telegram.js.map +1 -0
- package/dist/lib/messenger/types.d.ts +21 -0
- package/dist/lib/messenger/types.d.ts.map +1 -0
- package/dist/lib/messenger/types.js +2 -0
- package/dist/lib/messenger/types.js.map +1 -0
- package/dist/lib/messenger/whatsapp.d.ts +16 -0
- package/dist/lib/messenger/whatsapp.d.ts.map +1 -0
- package/dist/lib/messenger/whatsapp.js +103 -0
- package/dist/lib/messenger/whatsapp.js.map +1 -0
- package/dist/lib/rules.d.ts +17 -0
- package/dist/lib/rules.d.ts.map +1 -0
- package/dist/lib/rules.js +138 -0
- package/dist/lib/rules.js.map +1 -0
- package/dist/lib/rules.test.d.ts +2 -0
- package/dist/lib/rules.test.d.ts.map +1 -0
- package/dist/lib/rules.test.js +144 -0
- package/dist/lib/rules.test.js.map +1 -0
- package/dist/lib/setup-instructions.d.ts +3 -0
- package/dist/lib/setup-instructions.d.ts.map +1 -0
- package/dist/lib/setup-instructions.js +55 -0
- package/dist/lib/setup-instructions.js.map +1 -0
- package/dist/lib/slack.d.ts +18 -0
- package/dist/lib/slack.d.ts.map +1 -0
- package/dist/lib/slack.js +21 -0
- package/dist/lib/slack.js.map +1 -0
- package/dist/lib/supabase.d.ts +33 -0
- package/dist/lib/supabase.d.ts.map +1 -0
- package/dist/lib/supabase.js +169 -0
- package/dist/lib/supabase.js.map +1 -0
- package/package.json +67 -0
- package/supabase/functions/slack-callback/index.ts +198 -0
- package/supabase/functions/telegram-callback/index.ts +209 -0
- package/supabase/functions/whatsapp-callback/index.ts +180 -0
- package/supabase/migrations/001_create_approval_requests.sql +91 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { createClient } from '@supabase/supabase-js';
|
|
2
|
+
// Patterns that may contain sensitive information
|
|
3
|
+
const SENSITIVE_PATTERNS = [
|
|
4
|
+
{ pattern: /([?&])(api[_-]?key|token|secret|password|auth|key|access[_-]?token)=([^&\s'"]+)/gi, replacement: '$1$2=[REDACTED]' },
|
|
5
|
+
{ pattern: /(Authorization:\s*)(Bearer\s+)?[^\s'"]+/gi, replacement: '$1$2[REDACTED]' },
|
|
6
|
+
{ pattern: /(export\s+)?(AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID|API_KEY|SECRET_KEY|PRIVATE_KEY|DATABASE_URL|DB_PASSWORD|SLACK_TOKEN|GITHUB_TOKEN|NPM_TOKEN)=([^\s'"]+)/gi, replacement: '$1$2=[REDACTED]' },
|
|
7
|
+
{ pattern: /:\/\/([^:]+):([^@]+)@/g, replacement: '://$1:[REDACTED]@' },
|
|
8
|
+
{ pattern: /(Basic\s+)[A-Za-z0-9+/=]{20,}/gi, replacement: '$1[REDACTED]' },
|
|
9
|
+
];
|
|
10
|
+
function maskSensitiveInfo(text) {
|
|
11
|
+
let masked = text;
|
|
12
|
+
for (const { pattern, replacement } of SENSITIVE_PATTERNS) {
|
|
13
|
+
masked = masked.replace(pattern, replacement);
|
|
14
|
+
}
|
|
15
|
+
return masked;
|
|
16
|
+
}
|
|
17
|
+
let supabaseClient = null;
|
|
18
|
+
export function initializeSupabase(config) {
|
|
19
|
+
if (supabaseClient) {
|
|
20
|
+
return supabaseClient;
|
|
21
|
+
}
|
|
22
|
+
supabaseClient = createClient(config.supabase.url, config.supabase.anonKey);
|
|
23
|
+
return supabaseClient;
|
|
24
|
+
}
|
|
25
|
+
export function getSupabaseClient() {
|
|
26
|
+
if (!supabaseClient) {
|
|
27
|
+
throw new Error('Supabase not initialized. Call initializeSupabase first.');
|
|
28
|
+
}
|
|
29
|
+
return supabaseClient;
|
|
30
|
+
}
|
|
31
|
+
export async function createRequest(requestId, request) {
|
|
32
|
+
const client = getSupabaseClient();
|
|
33
|
+
// Mask sensitive information before storing in database
|
|
34
|
+
const maskedCommand = maskSensitiveInfo(request.command);
|
|
35
|
+
const { error } = await client.from('approval_requests').insert({
|
|
36
|
+
id: requestId,
|
|
37
|
+
command: maskedCommand,
|
|
38
|
+
danger_reason: request.dangerReason,
|
|
39
|
+
severity: request.severity,
|
|
40
|
+
cwd: request.cwd,
|
|
41
|
+
status: 'pending',
|
|
42
|
+
});
|
|
43
|
+
if (error) {
|
|
44
|
+
throw new Error(`Failed to create request: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export async function updateRequestStatus(requestId, status, resolvedBy) {
|
|
48
|
+
const client = getSupabaseClient();
|
|
49
|
+
const updates = {
|
|
50
|
+
status,
|
|
51
|
+
resolved_at: new Date().toISOString(),
|
|
52
|
+
};
|
|
53
|
+
if (resolvedBy) {
|
|
54
|
+
updates.resolved_by = resolvedBy;
|
|
55
|
+
}
|
|
56
|
+
const { error } = await client.from('approval_requests').update(updates).eq('id', requestId);
|
|
57
|
+
if (error) {
|
|
58
|
+
throw new Error(`Failed to update request status: ${error.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export async function getRequest(requestId) {
|
|
62
|
+
const client = getSupabaseClient();
|
|
63
|
+
const { data, error } = await client
|
|
64
|
+
.from('approval_requests')
|
|
65
|
+
.select('*')
|
|
66
|
+
.eq('id', requestId)
|
|
67
|
+
.single();
|
|
68
|
+
if (error) {
|
|
69
|
+
if (error.code === 'PGRST116') {
|
|
70
|
+
return null; // Not found
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`Failed to get request: ${error.message}`);
|
|
73
|
+
}
|
|
74
|
+
return data;
|
|
75
|
+
}
|
|
76
|
+
export function listenForApproval(requestId, timeoutMs, onResolved) {
|
|
77
|
+
const client = getSupabaseClient();
|
|
78
|
+
let resolved = false;
|
|
79
|
+
let timeoutId = null;
|
|
80
|
+
let channel = null;
|
|
81
|
+
// Subscribe to realtime changes
|
|
82
|
+
channel = client
|
|
83
|
+
.channel(`approval-${requestId}`)
|
|
84
|
+
.on('postgres_changes', {
|
|
85
|
+
event: 'UPDATE',
|
|
86
|
+
schema: 'public',
|
|
87
|
+
table: 'approval_requests',
|
|
88
|
+
filter: `id=eq.${requestId}`,
|
|
89
|
+
}, (payload) => {
|
|
90
|
+
const newStatus = payload.new.status;
|
|
91
|
+
if (newStatus && newStatus !== 'pending' && !resolved) {
|
|
92
|
+
resolved = true;
|
|
93
|
+
if (timeoutId) {
|
|
94
|
+
clearTimeout(timeoutId);
|
|
95
|
+
}
|
|
96
|
+
cleanup();
|
|
97
|
+
onResolved(newStatus);
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
.subscribe();
|
|
101
|
+
// Set timeout
|
|
102
|
+
timeoutId = setTimeout(async () => {
|
|
103
|
+
if (!resolved) {
|
|
104
|
+
resolved = true;
|
|
105
|
+
cleanup();
|
|
106
|
+
// Update status to timeout in Supabase
|
|
107
|
+
try {
|
|
108
|
+
await updateRequestStatus(requestId, 'timeout');
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
// Log error but don't block the timeout flow
|
|
112
|
+
console.error(`[Claude Guard] Failed to update timeout status for ${requestId}:`, err);
|
|
113
|
+
}
|
|
114
|
+
onResolved('timeout');
|
|
115
|
+
}
|
|
116
|
+
}, timeoutMs);
|
|
117
|
+
// Cleanup function
|
|
118
|
+
function cleanup() {
|
|
119
|
+
if (channel) {
|
|
120
|
+
client.removeChannel(channel);
|
|
121
|
+
channel = null;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// Return cleanup function
|
|
125
|
+
return () => {
|
|
126
|
+
if (!resolved) {
|
|
127
|
+
resolved = true;
|
|
128
|
+
if (timeoutId) {
|
|
129
|
+
clearTimeout(timeoutId);
|
|
130
|
+
}
|
|
131
|
+
cleanup();
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
export async function cleanupOldRequests(maxAgeMs = 24 * 60 * 60 * 1000) {
|
|
136
|
+
const client = getSupabaseClient();
|
|
137
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString();
|
|
138
|
+
const { data, error } = await client
|
|
139
|
+
.from('approval_requests')
|
|
140
|
+
.delete()
|
|
141
|
+
.lt('created_at', cutoff)
|
|
142
|
+
.select('id');
|
|
143
|
+
if (error) {
|
|
144
|
+
throw new Error(`Failed to cleanup old requests: ${error.message}`);
|
|
145
|
+
}
|
|
146
|
+
return data?.length ?? 0;
|
|
147
|
+
}
|
|
148
|
+
export async function testConnection(config) {
|
|
149
|
+
try {
|
|
150
|
+
const client = initializeSupabase(config);
|
|
151
|
+
// Try to query the table (will fail if table doesn't exist or no access)
|
|
152
|
+
const { error } = await client.from('approval_requests').select('id').limit(1);
|
|
153
|
+
if (error) {
|
|
154
|
+
return { ok: false, error: error.message };
|
|
155
|
+
}
|
|
156
|
+
return { ok: true };
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
160
|
+
return { ok: false, error: errorMessage };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
export async function shutdownSupabase() {
|
|
164
|
+
if (supabaseClient) {
|
|
165
|
+
await supabaseClient.removeAllChannels();
|
|
166
|
+
supabaseClient = null;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=supabase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"supabase.js","sourceRoot":"","sources":["../../src/lib/supabase.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-remote-guard",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remote approval system for Claude Code CLI using Slack and Supabase",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"bin": {
|
|
9
|
+
"guard": "dist/bin/cli.js",
|
|
10
|
+
"claude-guard-hook": "dist/bin/hook.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsc --watch",
|
|
15
|
+
"lint": "eslint src --ext .ts",
|
|
16
|
+
"lint:fix": "eslint src --ext .ts --fix",
|
|
17
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
18
|
+
"test": "vitest",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"claude",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"cli",
|
|
25
|
+
"hook",
|
|
26
|
+
"slack",
|
|
27
|
+
"supabase",
|
|
28
|
+
"approval",
|
|
29
|
+
"security"
|
|
30
|
+
],
|
|
31
|
+
"author": "paduck86",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+https://github.com/paduck86/claude-remote-guard.git"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/paduck86/claude-remote-guard#readme",
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/paduck86/claude-remote-guard/issues"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@supabase/supabase-js": "^2.39.0",
|
|
43
|
+
"chalk": "^5.3.0",
|
|
44
|
+
"commander": "^12.0.0",
|
|
45
|
+
"inquirer": "^9.2.0",
|
|
46
|
+
"uuid": "^9.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/inquirer": "^9.0.7",
|
|
50
|
+
"@types/node": "^20.11.0",
|
|
51
|
+
"@types/uuid": "^9.0.8",
|
|
52
|
+
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
|
53
|
+
"@typescript-eslint/parser": "^7.0.0",
|
|
54
|
+
"eslint": "^8.57.0",
|
|
55
|
+
"eslint-config-prettier": "^9.1.0",
|
|
56
|
+
"prettier": "^3.2.0",
|
|
57
|
+
"typescript": "^5.3.0",
|
|
58
|
+
"vitest": "^1.2.0"
|
|
59
|
+
},
|
|
60
|
+
"engines": {
|
|
61
|
+
"node": ">=18.0.0"
|
|
62
|
+
},
|
|
63
|
+
"files": [
|
|
64
|
+
"dist",
|
|
65
|
+
"supabase"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
// Supabase Edge Function for Slack Interactive Callbacks
|
|
2
|
+
// Deploy: supabase functions deploy slack-callback
|
|
3
|
+
//
|
|
4
|
+
// Required environment variables:
|
|
5
|
+
// - SLACK_SIGNING_SECRET: Your Slack app's signing secret
|
|
6
|
+
// - SUPABASE_URL: Auto-provided by Supabase
|
|
7
|
+
// - SUPABASE_SERVICE_ROLE_KEY: Auto-provided by Supabase
|
|
8
|
+
|
|
9
|
+
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
|
10
|
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
11
|
+
|
|
12
|
+
interface SlackAction {
|
|
13
|
+
action_id: string;
|
|
14
|
+
value: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface SlackUser {
|
|
18
|
+
id: string;
|
|
19
|
+
username: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface SlackPayload {
|
|
24
|
+
type: string;
|
|
25
|
+
user: SlackUser;
|
|
26
|
+
actions: SlackAction[];
|
|
27
|
+
response_url: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// HMAC-SHA256 signature verification for Slack requests
|
|
31
|
+
async function verifySlackSignature(
|
|
32
|
+
body: string,
|
|
33
|
+
timestamp: string,
|
|
34
|
+
signature: string,
|
|
35
|
+
signingSecret: string
|
|
36
|
+
): Promise<boolean> {
|
|
37
|
+
const encoder = new TextEncoder();
|
|
38
|
+
const key = await crypto.subtle.importKey(
|
|
39
|
+
'raw',
|
|
40
|
+
encoder.encode(signingSecret),
|
|
41
|
+
{ name: 'HMAC', hash: 'SHA-256' },
|
|
42
|
+
false,
|
|
43
|
+
['sign']
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const baseString = `v0:${timestamp}:${body}`;
|
|
47
|
+
const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(baseString));
|
|
48
|
+
const computedSignature = 'v0=' + Array.from(new Uint8Array(signatureBuffer))
|
|
49
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
50
|
+
.join('');
|
|
51
|
+
|
|
52
|
+
// Timing-safe comparison
|
|
53
|
+
if (computedSignature.length !== signature.length) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
let result = 0;
|
|
57
|
+
for (let i = 0; i < computedSignature.length; i++) {
|
|
58
|
+
result |= computedSignature.charCodeAt(i) ^ signature.charCodeAt(i);
|
|
59
|
+
}
|
|
60
|
+
return result === 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
serve(async (req: Request) => {
|
|
64
|
+
// Only allow POST from Slack
|
|
65
|
+
if (req.method !== 'POST') {
|
|
66
|
+
return new Response('Method not allowed', { status: 405 });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
// Get Slack signing secret
|
|
71
|
+
const signingSecret = Deno.env.get('SLACK_SIGNING_SECRET');
|
|
72
|
+
if (!signingSecret) {
|
|
73
|
+
console.error('Missing SLACK_SIGNING_SECRET environment variable');
|
|
74
|
+
return new Response('Server configuration error', { status: 500 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Get signature headers
|
|
78
|
+
const slackSignature = req.headers.get('x-slack-signature');
|
|
79
|
+
const slackTimestamp = req.headers.get('x-slack-request-timestamp');
|
|
80
|
+
|
|
81
|
+
if (!slackSignature || !slackTimestamp) {
|
|
82
|
+
console.error('Missing Slack signature headers');
|
|
83
|
+
return new Response('Unauthorized', { status: 401 });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check timestamp to prevent replay attacks (5 minute window)
|
|
87
|
+
const currentTime = Math.floor(Date.now() / 1000);
|
|
88
|
+
const requestTime = parseInt(slackTimestamp, 10);
|
|
89
|
+
if (Math.abs(currentTime - requestTime) > 300) {
|
|
90
|
+
console.error('Request timestamp too old');
|
|
91
|
+
return new Response('Unauthorized', { status: 401 });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Read body as text for signature verification
|
|
95
|
+
const bodyText = await req.text();
|
|
96
|
+
|
|
97
|
+
// Verify signature
|
|
98
|
+
const isValid = await verifySlackSignature(bodyText, slackTimestamp, slackSignature, signingSecret);
|
|
99
|
+
if (!isValid) {
|
|
100
|
+
console.error('Invalid Slack signature');
|
|
101
|
+
return new Response('Unauthorized', { status: 401 });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Parse form data from body text
|
|
105
|
+
const params = new URLSearchParams(bodyText);
|
|
106
|
+
const payloadStr = params.get('payload');
|
|
107
|
+
|
|
108
|
+
if (!payloadStr) {
|
|
109
|
+
return new Response('Invalid payload', { status: 400 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const payload: SlackPayload = JSON.parse(payloadStr);
|
|
113
|
+
|
|
114
|
+
// Validate payload type
|
|
115
|
+
if (payload.type !== 'block_actions') {
|
|
116
|
+
return new Response('Unsupported interaction type', { status: 400 });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Get action details
|
|
120
|
+
const action = payload.actions[0];
|
|
121
|
+
if (!action) {
|
|
122
|
+
return new Response('No action found', { status: 400 });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { action_id, value: requestId } = action;
|
|
126
|
+
const resolvedBy = payload.user.username || payload.user.name || payload.user.id;
|
|
127
|
+
|
|
128
|
+
// Determine status based on action
|
|
129
|
+
let status: 'approved' | 'rejected';
|
|
130
|
+
if (action_id === 'approve_command') {
|
|
131
|
+
status = 'approved';
|
|
132
|
+
} else if (action_id === 'reject_command') {
|
|
133
|
+
status = 'rejected';
|
|
134
|
+
} else {
|
|
135
|
+
return new Response('Unknown action', { status: 400 });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Initialize Supabase client with service role key
|
|
139
|
+
const supabaseUrl = Deno.env.get('SUPABASE_URL');
|
|
140
|
+
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
|
|
141
|
+
|
|
142
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
143
|
+
console.error('Missing Supabase environment variables');
|
|
144
|
+
return new Response('Server configuration error', { status: 500 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
148
|
+
|
|
149
|
+
// Update the approval request
|
|
150
|
+
const { data, error } = await supabase
|
|
151
|
+
.from('approval_requests')
|
|
152
|
+
.update({
|
|
153
|
+
status,
|
|
154
|
+
resolved_at: new Date().toISOString(),
|
|
155
|
+
resolved_by: resolvedBy,
|
|
156
|
+
})
|
|
157
|
+
.eq('id', requestId)
|
|
158
|
+
.eq('status', 'pending') // Only update if still pending
|
|
159
|
+
.select('id');
|
|
160
|
+
|
|
161
|
+
if (error) {
|
|
162
|
+
console.error('Failed to update request:', error);
|
|
163
|
+
return new Response('Failed to update request', { status: 500 });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Verify that a row was actually updated
|
|
167
|
+
if (!data || data.length === 0) {
|
|
168
|
+
console.error('Request not found or already resolved:', requestId);
|
|
169
|
+
return new Response('Request not found or already resolved', { status: 404 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Send response back to Slack to update the message
|
|
173
|
+
const responseMessage =
|
|
174
|
+
status === 'approved'
|
|
175
|
+
? `:white_check_mark: *Approved* by @${resolvedBy}`
|
|
176
|
+
: `:x: *Rejected* by @${resolvedBy}`;
|
|
177
|
+
|
|
178
|
+
// Respond to Slack's response_url to update the original message
|
|
179
|
+
if (payload.response_url) {
|
|
180
|
+
await fetch(payload.response_url, {
|
|
181
|
+
method: 'POST',
|
|
182
|
+
headers: { 'Content-Type': 'application/json' },
|
|
183
|
+
body: JSON.stringify({
|
|
184
|
+
replace_original: false,
|
|
185
|
+
text: responseMessage,
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return new Response('OK', {
|
|
191
|
+
status: 200,
|
|
192
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error processing request:', error);
|
|
196
|
+
return new Response('Internal server error', { status: 500 });
|
|
197
|
+
}
|
|
198
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// Supabase Edge Function for Telegram Bot Callbacks
|
|
2
|
+
// Deploy: supabase functions deploy telegram-callback
|
|
3
|
+
//
|
|
4
|
+
// Required environment variables:
|
|
5
|
+
// - TELEGRAM_BOT_TOKEN: Your Telegram bot token
|
|
6
|
+
// - TELEGRAM_WEBHOOK_SECRET: Secret token for webhook verification (set via setWebhook API)
|
|
7
|
+
// - SUPABASE_URL: Auto-provided by Supabase
|
|
8
|
+
// - SUPABASE_SERVICE_ROLE_KEY: Auto-provided by Supabase
|
|
9
|
+
|
|
10
|
+
import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
|
|
11
|
+
import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
|
|
12
|
+
|
|
13
|
+
// Timing-safe string comparison to prevent timing attacks
|
|
14
|
+
function timingSafeEqual(a: string, b: string): boolean {
|
|
15
|
+
if (a.length !== b.length) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
let result = 0;
|
|
19
|
+
for (let i = 0; i < a.length; i++) {
|
|
20
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
21
|
+
}
|
|
22
|
+
return result === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface TelegramUser {
|
|
26
|
+
id: number;
|
|
27
|
+
first_name: string;
|
|
28
|
+
last_name?: string;
|
|
29
|
+
username?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface TelegramMessage {
|
|
33
|
+
message_id: number;
|
|
34
|
+
chat: {
|
|
35
|
+
id: number;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface CallbackQuery {
|
|
40
|
+
id: string;
|
|
41
|
+
from: TelegramUser;
|
|
42
|
+
message?: TelegramMessage;
|
|
43
|
+
data?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface TelegramUpdate {
|
|
47
|
+
update_id: number;
|
|
48
|
+
callback_query?: CallbackQuery;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
serve(async (req: Request) => {
|
|
52
|
+
// Only allow POST
|
|
53
|
+
if (req.method !== 'POST') {
|
|
54
|
+
return new Response('Method not allowed', { status: 405 });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const botToken = Deno.env.get('TELEGRAM_BOT_TOKEN');
|
|
59
|
+
if (!botToken) {
|
|
60
|
+
console.error('Missing TELEGRAM_BOT_TOKEN environment variable');
|
|
61
|
+
return new Response('Server configuration error', { status: 500 });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Verify webhook secret token (set via setWebhook API with secret_token parameter)
|
|
65
|
+
const webhookSecret = Deno.env.get('TELEGRAM_WEBHOOK_SECRET');
|
|
66
|
+
if (!webhookSecret) {
|
|
67
|
+
console.error('Missing TELEGRAM_WEBHOOK_SECRET environment variable');
|
|
68
|
+
return new Response('Server configuration error', { status: 500 });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const headerToken = req.headers.get('X-Telegram-Bot-Api-Secret-Token');
|
|
72
|
+
if (!headerToken || !timingSafeEqual(headerToken, webhookSecret)) {
|
|
73
|
+
console.error('Invalid or missing Telegram webhook secret token');
|
|
74
|
+
return new Response('Unauthorized', { status: 401 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const update: TelegramUpdate = await req.json();
|
|
78
|
+
|
|
79
|
+
// Only handle callback queries (button presses)
|
|
80
|
+
if (!update.callback_query) {
|
|
81
|
+
return new Response('OK', { status: 200 });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const callbackQuery = update.callback_query;
|
|
85
|
+
const callbackData = callbackQuery.data;
|
|
86
|
+
|
|
87
|
+
if (!callbackData) {
|
|
88
|
+
return new Response('No callback data', { status: 400 });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Parse callback data: "approve:requestId" or "reject:requestId"
|
|
92
|
+
const [action, requestId] = callbackData.split(':');
|
|
93
|
+
|
|
94
|
+
if (!action || !requestId) {
|
|
95
|
+
return new Response('Invalid callback data format', { status: 400 });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (action !== 'approve' && action !== 'reject') {
|
|
99
|
+
return new Response('Unknown action', { status: 400 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const status = action === 'approve' ? 'approved' : 'rejected';
|
|
103
|
+
const resolvedBy = callbackQuery.from.username ||
|
|
104
|
+
`${callbackQuery.from.first_name}${callbackQuery.from.last_name ? ' ' + callbackQuery.from.last_name : ''}` ||
|
|
105
|
+
String(callbackQuery.from.id);
|
|
106
|
+
|
|
107
|
+
// Initialize Supabase client
|
|
108
|
+
const supabaseUrl = Deno.env.get('SUPABASE_URL');
|
|
109
|
+
const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
|
|
110
|
+
|
|
111
|
+
if (!supabaseUrl || !supabaseServiceKey) {
|
|
112
|
+
console.error('Missing Supabase environment variables');
|
|
113
|
+
return new Response('Server configuration error', { status: 500 });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const supabase = createClient(supabaseUrl, supabaseServiceKey);
|
|
117
|
+
|
|
118
|
+
// Update the approval request
|
|
119
|
+
const { data, error } = await supabase
|
|
120
|
+
.from('approval_requests')
|
|
121
|
+
.update({
|
|
122
|
+
status,
|
|
123
|
+
resolved_at: new Date().toISOString(),
|
|
124
|
+
resolved_by: resolvedBy,
|
|
125
|
+
})
|
|
126
|
+
.eq('id', requestId)
|
|
127
|
+
.eq('status', 'pending')
|
|
128
|
+
.select('id');
|
|
129
|
+
|
|
130
|
+
if (error) {
|
|
131
|
+
console.error('Failed to update request:', error);
|
|
132
|
+
// Answer callback query with error
|
|
133
|
+
await answerCallbackQuery(botToken, callbackQuery.id, '❌ Failed to update request');
|
|
134
|
+
return new Response('Failed to update request', { status: 500 });
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Check if request was found and updated
|
|
138
|
+
if (!data || data.length === 0) {
|
|
139
|
+
await answerCallbackQuery(botToken, callbackQuery.id, '⚠️ Request not found or already resolved');
|
|
140
|
+
return new Response('OK', { status: 200 });
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Answer callback query with success
|
|
144
|
+
const emoji = status === 'approved' ? '✅' : '❌';
|
|
145
|
+
const actionText = status === 'approved' ? 'Approved' : 'Rejected';
|
|
146
|
+
await answerCallbackQuery(botToken, callbackQuery.id, `${emoji} ${actionText}`);
|
|
147
|
+
|
|
148
|
+
// Edit original message to remove buttons and show result
|
|
149
|
+
if (callbackQuery.message) {
|
|
150
|
+
await editMessageReplyMarkup(
|
|
151
|
+
botToken,
|
|
152
|
+
callbackQuery.message.chat.id,
|
|
153
|
+
callbackQuery.message.message_id,
|
|
154
|
+
`\n\n${emoji} *${actionText}* by @${resolvedBy}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return new Response('OK', { status: 200 });
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error processing request:', error);
|
|
161
|
+
return new Response('Internal server error', { status: 500 });
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
async function answerCallbackQuery(
|
|
166
|
+
botToken: string,
|
|
167
|
+
callbackQueryId: string,
|
|
168
|
+
text: string
|
|
169
|
+
): Promise<void> {
|
|
170
|
+
await fetch(`https://api.telegram.org/bot${botToken}/answerCallbackQuery`, {
|
|
171
|
+
method: 'POST',
|
|
172
|
+
headers: { 'Content-Type': 'application/json' },
|
|
173
|
+
body: JSON.stringify({
|
|
174
|
+
callback_query_id: callbackQueryId,
|
|
175
|
+
text,
|
|
176
|
+
show_alert: false,
|
|
177
|
+
}),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function editMessageReplyMarkup(
|
|
182
|
+
botToken: string,
|
|
183
|
+
chatId: number,
|
|
184
|
+
messageId: number,
|
|
185
|
+
appendText: string
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
// Remove inline keyboard
|
|
188
|
+
await fetch(`https://api.telegram.org/bot${botToken}/editMessageReplyMarkup`, {
|
|
189
|
+
method: 'POST',
|
|
190
|
+
headers: { 'Content-Type': 'application/json' },
|
|
191
|
+
body: JSON.stringify({
|
|
192
|
+
chat_id: chatId,
|
|
193
|
+
message_id: messageId,
|
|
194
|
+
reply_markup: { inline_keyboard: [] },
|
|
195
|
+
}),
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Send follow-up message with result
|
|
199
|
+
await fetch(`https://api.telegram.org/bot${botToken}/sendMessage`, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: { 'Content-Type': 'application/json' },
|
|
202
|
+
body: JSON.stringify({
|
|
203
|
+
chat_id: chatId,
|
|
204
|
+
reply_to_message_id: messageId,
|
|
205
|
+
text: appendText.replace(/[_*\[\]()~`>#+\-=|{}.!\\]/g, '\\$&'),
|
|
206
|
+
parse_mode: 'MarkdownV2',
|
|
207
|
+
}),
|
|
208
|
+
});
|
|
209
|
+
}
|