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.
Files changed (82) hide show
  1. package/README.md +433 -0
  2. package/dist/bin/cli.d.ts +3 -0
  3. package/dist/bin/cli.d.ts.map +1 -0
  4. package/dist/bin/cli.js +427 -0
  5. package/dist/bin/cli.js.map +1 -0
  6. package/dist/bin/hook.d.ts +3 -0
  7. package/dist/bin/hook.d.ts.map +1 -0
  8. package/dist/bin/hook.js +136 -0
  9. package/dist/bin/hook.js.map +1 -0
  10. package/dist/index.d.ts +6 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +6 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/lib/claude-settings.d.ts +11 -0
  15. package/dist/lib/claude-settings.d.ts.map +1 -0
  16. package/dist/lib/claude-settings.js +96 -0
  17. package/dist/lib/claude-settings.js.map +1 -0
  18. package/dist/lib/config.d.ts +47 -0
  19. package/dist/lib/config.d.ts.map +1 -0
  20. package/dist/lib/config.js +177 -0
  21. package/dist/lib/config.js.map +1 -0
  22. package/dist/lib/edge-function.d.ts +14 -0
  23. package/dist/lib/edge-function.d.ts.map +1 -0
  24. package/dist/lib/edge-function.js +521 -0
  25. package/dist/lib/edge-function.js.map +1 -0
  26. package/dist/lib/firebase.d.ts +27 -0
  27. package/dist/lib/firebase.d.ts.map +1 -0
  28. package/dist/lib/firebase.js +136 -0
  29. package/dist/lib/firebase.js.map +1 -0
  30. package/dist/lib/messenger/base.d.ts +6 -0
  31. package/dist/lib/messenger/base.d.ts.map +1 -0
  32. package/dist/lib/messenger/base.js +34 -0
  33. package/dist/lib/messenger/base.js.map +1 -0
  34. package/dist/lib/messenger/factory.d.ts +15 -0
  35. package/dist/lib/messenger/factory.d.ts.map +1 -0
  36. package/dist/lib/messenger/factory.js +37 -0
  37. package/dist/lib/messenger/factory.js.map +1 -0
  38. package/dist/lib/messenger/index.d.ts +7 -0
  39. package/dist/lib/messenger/index.d.ts.map +1 -0
  40. package/dist/lib/messenger/index.js +9 -0
  41. package/dist/lib/messenger/index.js.map +1 -0
  42. package/dist/lib/messenger/slack.d.ts +14 -0
  43. package/dist/lib/messenger/slack.d.ts.map +1 -0
  44. package/dist/lib/messenger/slack.js +169 -0
  45. package/dist/lib/messenger/slack.js.map +1 -0
  46. package/dist/lib/messenger/telegram.d.ts +15 -0
  47. package/dist/lib/messenger/telegram.d.ts.map +1 -0
  48. package/dist/lib/messenger/telegram.js +120 -0
  49. package/dist/lib/messenger/telegram.js.map +1 -0
  50. package/dist/lib/messenger/types.d.ts +21 -0
  51. package/dist/lib/messenger/types.d.ts.map +1 -0
  52. package/dist/lib/messenger/types.js +2 -0
  53. package/dist/lib/messenger/types.js.map +1 -0
  54. package/dist/lib/messenger/whatsapp.d.ts +16 -0
  55. package/dist/lib/messenger/whatsapp.d.ts.map +1 -0
  56. package/dist/lib/messenger/whatsapp.js +103 -0
  57. package/dist/lib/messenger/whatsapp.js.map +1 -0
  58. package/dist/lib/rules.d.ts +17 -0
  59. package/dist/lib/rules.d.ts.map +1 -0
  60. package/dist/lib/rules.js +138 -0
  61. package/dist/lib/rules.js.map +1 -0
  62. package/dist/lib/rules.test.d.ts +2 -0
  63. package/dist/lib/rules.test.d.ts.map +1 -0
  64. package/dist/lib/rules.test.js +144 -0
  65. package/dist/lib/rules.test.js.map +1 -0
  66. package/dist/lib/setup-instructions.d.ts +3 -0
  67. package/dist/lib/setup-instructions.d.ts.map +1 -0
  68. package/dist/lib/setup-instructions.js +55 -0
  69. package/dist/lib/setup-instructions.js.map +1 -0
  70. package/dist/lib/slack.d.ts +18 -0
  71. package/dist/lib/slack.d.ts.map +1 -0
  72. package/dist/lib/slack.js +21 -0
  73. package/dist/lib/slack.js.map +1 -0
  74. package/dist/lib/supabase.d.ts +33 -0
  75. package/dist/lib/supabase.d.ts.map +1 -0
  76. package/dist/lib/supabase.js +169 -0
  77. package/dist/lib/supabase.js.map +1 -0
  78. package/package.json +67 -0
  79. package/supabase/functions/slack-callback/index.ts +198 -0
  80. package/supabase/functions/telegram-callback/index.ts +209 -0
  81. package/supabase/functions/whatsapp-callback/index.ts +180 -0
  82. 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
+ }