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,180 @@
1
+ // Supabase Edge Function for WhatsApp (Twilio) Callbacks
2
+ // Deploy: supabase functions deploy whatsapp-callback
3
+ //
4
+ // Required environment variables:
5
+ // - TWILIO_AUTH_TOKEN: Your Twilio auth token for signature verification
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
+ // Timing-safe string comparison to prevent timing attacks
13
+ function timingSafeEqual(a: string, b: string): boolean {
14
+ if (a.length !== b.length) {
15
+ return false;
16
+ }
17
+ let result = 0;
18
+ for (let i = 0; i < a.length; i++) {
19
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
20
+ }
21
+ return result === 0;
22
+ }
23
+
24
+ // Verify Twilio request signature
25
+ async function verifyTwilioSignature(
26
+ authToken: string,
27
+ signature: string,
28
+ url: string,
29
+ params: Record<string, string>
30
+ ): Promise<boolean> {
31
+ // Sort parameters and create string
32
+ const sortedParams = Object.keys(params)
33
+ .sort()
34
+ .map(key => `${key}${params[key]}`)
35
+ .join('');
36
+
37
+ const data = url + sortedParams;
38
+
39
+ const encoder = new TextEncoder();
40
+ const key = await crypto.subtle.importKey(
41
+ 'raw',
42
+ encoder.encode(authToken),
43
+ { name: 'HMAC', hash: 'SHA-1' },
44
+ false,
45
+ ['sign']
46
+ );
47
+
48
+ const signatureBuffer = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
49
+ const computedSignature = btoa(String.fromCharCode(...new Uint8Array(signatureBuffer)));
50
+
51
+ // Timing-safe comparison
52
+ return timingSafeEqual(signature, computedSignature);
53
+ }
54
+
55
+ serve(async (req: Request) => {
56
+ // Only allow POST
57
+ if (req.method !== 'POST') {
58
+ return new Response('Method not allowed', { status: 405 });
59
+ }
60
+
61
+ try {
62
+ const authToken = Deno.env.get('TWILIO_AUTH_TOKEN');
63
+ if (!authToken) {
64
+ console.error('Missing TWILIO_AUTH_TOKEN environment variable');
65
+ return new Response('Server configuration error', { status: 500 });
66
+ }
67
+
68
+ // Parse form data
69
+ const formData = await req.formData();
70
+ const params: Record<string, string> = {};
71
+ formData.forEach((value, key) => {
72
+ params[key] = value.toString();
73
+ });
74
+
75
+ // Verify Twilio signature (required for security)
76
+ const twilioSignature = req.headers.get('X-Twilio-Signature');
77
+ if (!twilioSignature) {
78
+ console.error('Missing Twilio signature header');
79
+ return new Response('Unauthorized', { status: 401 });
80
+ }
81
+
82
+ const isValid = await verifyTwilioSignature(
83
+ authToken,
84
+ twilioSignature,
85
+ req.url,
86
+ params
87
+ );
88
+ if (!isValid) {
89
+ console.error('Invalid Twilio signature');
90
+ return new Response('Unauthorized', { status: 401 });
91
+ }
92
+
93
+ // Get message body
94
+ const body = params['Body']?.trim();
95
+ const from = params['From'];
96
+
97
+ if (!body) {
98
+ return twimlResponse('No message body received.');
99
+ }
100
+
101
+ // Parse command: "APPROVE <requestId>" or "REJECT <requestId>"
102
+ const match = body.match(/^(APPROVE|REJECT)\s+([a-f0-9-]+)$/i);
103
+
104
+ if (!match) {
105
+ return twimlResponse(
106
+ 'Invalid format. Use:\nAPPROVE <request-id>\nor\nREJECT <request-id>'
107
+ );
108
+ }
109
+
110
+ const [, action, requestId] = match;
111
+ const status = action.toUpperCase() === 'APPROVE' ? 'approved' : 'rejected';
112
+
113
+ // Extract phone number for resolved_by
114
+ const resolvedBy = from?.replace('whatsapp:', '') || 'unknown';
115
+
116
+ // Initialize Supabase client
117
+ const supabaseUrl = Deno.env.get('SUPABASE_URL');
118
+ const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
119
+
120
+ if (!supabaseUrl || !supabaseServiceKey) {
121
+ console.error('Missing Supabase environment variables');
122
+ return twimlResponse('Server configuration error.');
123
+ }
124
+
125
+ const supabase = createClient(supabaseUrl, supabaseServiceKey);
126
+
127
+ // Update the approval request
128
+ const { data, error } = await supabase
129
+ .from('approval_requests')
130
+ .update({
131
+ status,
132
+ resolved_at: new Date().toISOString(),
133
+ resolved_by: resolvedBy,
134
+ })
135
+ .eq('id', requestId)
136
+ .eq('status', 'pending')
137
+ .select('id');
138
+
139
+ if (error) {
140
+ console.error('Failed to update request:', error);
141
+ return twimlResponse('Failed to update request. Please try again.');
142
+ }
143
+
144
+ if (!data || data.length === 0) {
145
+ return twimlResponse('Request not found or already resolved.');
146
+ }
147
+
148
+ // Send success response
149
+ const emoji = status === 'approved' ? '✅' : '❌';
150
+ const actionText = status === 'approved' ? 'approved' : 'rejected';
151
+ return twimlResponse(`${emoji} Request ${requestId.substring(0, 8)}... has been ${actionText}.`);
152
+
153
+ } catch (error) {
154
+ console.error('Error processing request:', error);
155
+ return twimlResponse('Internal server error.');
156
+ }
157
+ });
158
+
159
+ function twimlResponse(message: string): Response {
160
+ const twiml = `<?xml version="1.0" encoding="UTF-8"?>
161
+ <Response>
162
+ <Message>${escapeXml(message)}</Message>
163
+ </Response>`;
164
+
165
+ return new Response(twiml, {
166
+ status: 200,
167
+ headers: {
168
+ 'Content-Type': 'application/xml',
169
+ },
170
+ });
171
+ }
172
+
173
+ function escapeXml(text: string): string {
174
+ return text
175
+ .replace(/&/g, '&amp;')
176
+ .replace(/</g, '&lt;')
177
+ .replace(/>/g, '&gt;')
178
+ .replace(/"/g, '&quot;')
179
+ .replace(/'/g, '&apos;');
180
+ }
@@ -0,0 +1,91 @@
1
+ -- Create approval_requests table with proper security
2
+ -- Run this in your Supabase SQL editor
3
+
4
+ -- Create the table
5
+ CREATE TABLE IF NOT EXISTS approval_requests (
6
+ id UUID PRIMARY KEY,
7
+ command TEXT NOT NULL,
8
+ danger_reason TEXT NOT NULL,
9
+ severity TEXT NOT NULL CHECK (severity IN ('low', 'medium', 'high', 'critical')),
10
+ cwd TEXT NOT NULL,
11
+ status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'approved', 'rejected', 'timeout')),
12
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
13
+ resolved_at TIMESTAMPTZ,
14
+ resolved_by TEXT,
15
+ -- Add machine identifier for multi-user scenarios
16
+ machine_id TEXT,
17
+ -- Add index for faster queries
18
+ CONSTRAINT valid_resolution CHECK (
19
+ (status = 'pending' AND resolved_at IS NULL AND resolved_by IS NULL) OR
20
+ (status != 'pending' AND resolved_at IS NOT NULL)
21
+ )
22
+ );
23
+
24
+ -- Create indexes for common queries
25
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_status ON approval_requests(status);
26
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_created_at ON approval_requests(created_at);
27
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_machine_id ON approval_requests(machine_id);
28
+
29
+ -- Enable Row Level Security
30
+ ALTER TABLE approval_requests ENABLE ROW LEVEL SECURITY;
31
+
32
+ -- Drop existing policies if any
33
+ DROP POLICY IF EXISTS "Allow insert for authenticated and anon" ON approval_requests;
34
+ DROP POLICY IF EXISTS "Allow select own requests" ON approval_requests;
35
+ DROP POLICY IF EXISTS "Allow update via service role only" ON approval_requests;
36
+ DROP POLICY IF EXISTS "Allow delete old requests" ON approval_requests;
37
+
38
+ -- Policy: Allow insert (CLI creates requests)
39
+ -- In production, consider adding machine_id validation
40
+ CREATE POLICY "Allow insert for authenticated and anon" ON approval_requests
41
+ FOR INSERT
42
+ WITH CHECK (
43
+ status = 'pending' AND
44
+ resolved_at IS NULL AND
45
+ resolved_by IS NULL
46
+ );
47
+
48
+ -- Policy: Allow select own pending requests only (for real-time subscription)
49
+ -- Clients can only see their own pending requests
50
+ CREATE POLICY "Allow select pending requests" ON approval_requests
51
+ FOR SELECT
52
+ USING (
53
+ status = 'pending' AND
54
+ created_at > NOW() - INTERVAL '1 hour'
55
+ );
56
+
57
+ -- Policy: Only service role can update (Edge Function uses service role key)
58
+ -- This prevents unauthorized approval/rejection via anon key
59
+ CREATE POLICY "Allow update via service role only" ON approval_requests
60
+ FOR UPDATE
61
+ USING (auth.role() = 'service_role');
62
+
63
+ -- Policy: Allow cleanup of old requests
64
+ CREATE POLICY "Allow delete old requests" ON approval_requests
65
+ FOR DELETE
66
+ USING (created_at < NOW() - INTERVAL '24 hours');
67
+
68
+ -- Enable realtime for the table
69
+ ALTER PUBLICATION supabase_realtime ADD TABLE approval_requests;
70
+
71
+ -- Create a function to auto-cleanup old requests (optional)
72
+ CREATE OR REPLACE FUNCTION cleanup_old_approval_requests()
73
+ RETURNS void
74
+ LANGUAGE plpgsql
75
+ SECURITY DEFINER
76
+ AS $$
77
+ BEGIN
78
+ DELETE FROM approval_requests
79
+ WHERE created_at < NOW() - INTERVAL '7 days';
80
+ END;
81
+ $$;
82
+
83
+ -- Grant necessary permissions
84
+ GRANT SELECT, INSERT, DELETE ON approval_requests TO anon;
85
+ GRANT SELECT, INSERT, UPDATE, DELETE ON approval_requests TO authenticated;
86
+ GRANT ALL ON approval_requests TO service_role;
87
+
88
+ -- Comment for documentation
89
+ COMMENT ON TABLE approval_requests IS 'Stores pending command approval requests from Claude Guard CLI';
90
+ COMMENT ON COLUMN approval_requests.command IS 'The command that requires approval (sensitive info masked)';
91
+ COMMENT ON COLUMN approval_requests.machine_id IS 'Optional identifier to scope requests per machine';