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,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, '&')
|
|
176
|
+
.replace(/</g, '<')
|
|
177
|
+
.replace(/>/g, '>')
|
|
178
|
+
.replace(/"/g, '"')
|
|
179
|
+
.replace(/'/g, ''');
|
|
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';
|