realtimex-crm 0.7.14 → 0.9.1

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 (32) hide show
  1. package/dist/assets/{DealList-CZvo7G6M.js → DealList-DnGVfS15.js} +2 -2
  2. package/dist/assets/{DealList-CZvo7G6M.js.map → DealList-DnGVfS15.js.map} +1 -1
  3. package/dist/assets/index-DPrpo5Xq.js +159 -0
  4. package/dist/assets/index-DPrpo5Xq.js.map +1 -0
  5. package/dist/assets/index-kM1Og1AS.css +1 -0
  6. package/dist/index.html +1 -1
  7. package/dist/stats.html +1 -1
  8. package/package.json +2 -1
  9. package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
  10. package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
  11. package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +34 -0
  12. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
  13. package/src/components/atomic-crm/layout/Header.tsx +14 -1
  14. package/src/components/atomic-crm/root/CRM.tsx +2 -0
  15. package/src/components/supabase/change-password-page.tsx +9 -1
  16. package/src/components/supabase/forgot-password-page.tsx +30 -0
  17. package/src/components/supabase/layout.tsx +6 -2
  18. package/src/components/ui/alert-dialog.tsx +155 -0
  19. package/src/lib/api-key-utils.ts +22 -0
  20. package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
  21. package/supabase/functions/_shared/webhookSignature.ts +23 -0
  22. package/supabase/functions/api-v1-activities/index.ts +137 -0
  23. package/supabase/functions/api-v1-companies/index.ts +166 -0
  24. package/supabase/functions/api-v1-contacts/index.ts +171 -0
  25. package/supabase/functions/api-v1-deals/index.ts +166 -0
  26. package/supabase/functions/webhook-dispatcher/index.ts +133 -0
  27. package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
  28. package/supabase/migrations/20251219120100_webhook_triggers.sql +171 -0
  29. package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
  30. package/dist/assets/index-CdoQZFIX.css +0 -1
  31. package/dist/assets/index-D0sWLaB1.js +0 -153
  32. package/dist/assets/index-D0sWLaB1.js.map +0 -1
@@ -0,0 +1,171 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
3
+ import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
4
+ import {
5
+ validateApiKey,
6
+ checkRateLimit,
7
+ hasScope,
8
+ logApiRequest,
9
+ } from "../_shared/apiKeyAuth.ts";
10
+
11
+ Deno.serve(async (req: Request) => {
12
+ const startTime = Date.now();
13
+
14
+ // Handle CORS
15
+ if (req.method === "OPTIONS") {
16
+ return new Response(null, { status: 204, headers: corsHeaders });
17
+ }
18
+
19
+ // Validate API key
20
+ const authResult = await validateApiKey(req);
21
+ if ("status" in authResult) {
22
+ return authResult; // Error response
23
+ }
24
+ const { apiKey } = authResult;
25
+
26
+ // Check rate limit
27
+ const rateLimitError = checkRateLimit(apiKey.id);
28
+ if (rateLimitError) {
29
+ await logApiRequest(
30
+ apiKey.id,
31
+ "/v1/contacts",
32
+ req.method,
33
+ 429,
34
+ Date.now() - startTime,
35
+ req
36
+ );
37
+ return rateLimitError;
38
+ }
39
+
40
+ try {
41
+ const url = new URL(req.url);
42
+ const pathParts = url.pathname.split("/").filter(Boolean);
43
+ // pathParts: ["api-v1-contacts", "{id}"]
44
+ const contactId = pathParts[1];
45
+
46
+ let response: Response;
47
+
48
+ if (req.method === "GET" && contactId) {
49
+ response = await getContact(apiKey, contactId);
50
+ } else if (req.method === "POST") {
51
+ response = await createContact(apiKey, req);
52
+ } else if (req.method === "PATCH" && contactId) {
53
+ response = await updateContact(apiKey, contactId, req);
54
+ } else if (req.method === "DELETE" && contactId) {
55
+ response = await deleteContact(apiKey, contactId);
56
+ } else {
57
+ response = createErrorResponse(404, "Not found");
58
+ }
59
+
60
+ const responseTime = Date.now() - startTime;
61
+ await logApiRequest(
62
+ apiKey.id,
63
+ url.pathname,
64
+ req.method,
65
+ response.status,
66
+ responseTime,
67
+ req
68
+ );
69
+
70
+ return response;
71
+ } catch (error) {
72
+ const responseTime = Date.now() - startTime;
73
+ await logApiRequest(
74
+ apiKey.id,
75
+ new URL(req.url).pathname,
76
+ req.method,
77
+ 500,
78
+ responseTime,
79
+ req,
80
+ error.message
81
+ );
82
+ return createErrorResponse(500, "Internal server error");
83
+ }
84
+ });
85
+
86
+ async function getContact(apiKey: any, contactId: string) {
87
+ if (!hasScope(apiKey, "contacts:read")) {
88
+ return createErrorResponse(403, "Insufficient permissions");
89
+ }
90
+
91
+ const id = parseInt(contactId, 10);
92
+ const { data, error } = await supabaseAdmin
93
+ .from("contacts")
94
+ .select("*")
95
+ .eq("id", id)
96
+ .single();
97
+
98
+ if (error || !data) {
99
+ return createErrorResponse(404, "Contact not found");
100
+ }
101
+
102
+ return new Response(JSON.stringify({ data }), {
103
+ headers: { "Content-Type": "application/json", ...corsHeaders },
104
+ });
105
+ }
106
+
107
+ async function createContact(apiKey: any, req: Request) {
108
+ if (!hasScope(apiKey, "contacts:write")) {
109
+ return createErrorResponse(403, "Insufficient permissions");
110
+ }
111
+
112
+ const body = await req.json();
113
+
114
+ const { data, error } = await supabaseAdmin
115
+ .from("contacts")
116
+ .insert({
117
+ ...body,
118
+ sales_id: apiKey.sales_id, // Associate with API key owner
119
+ })
120
+ .select()
121
+ .single();
122
+
123
+ if (error) {
124
+ return createErrorResponse(400, error.message);
125
+ }
126
+
127
+ return new Response(JSON.stringify({ data }), {
128
+ status: 201,
129
+ headers: { "Content-Type": "application/json", ...corsHeaders },
130
+ });
131
+ }
132
+
133
+ async function updateContact(apiKey: any, contactId: string, req: Request) {
134
+ if (!hasScope(apiKey, "contacts:write")) {
135
+ return createErrorResponse(403, "Insufficient permissions");
136
+ }
137
+
138
+ const body = await req.json();
139
+
140
+ const { data, error } = await supabaseAdmin
141
+ .from("contacts")
142
+ .update(body)
143
+ .eq("id", parseInt(contactId, 10))
144
+ .select()
145
+ .single();
146
+
147
+ if (error || !data) {
148
+ return createErrorResponse(404, "Contact not found");
149
+ }
150
+
151
+ return new Response(JSON.stringify({ data }), {
152
+ headers: { "Content-Type": "application/json", ...corsHeaders },
153
+ });
154
+ }
155
+
156
+ async function deleteContact(apiKey: any, contactId: string) {
157
+ if (!hasScope(apiKey, "contacts:write")) {
158
+ return createErrorResponse(403, "Insufficient permissions");
159
+ }
160
+
161
+ const { error } = await supabaseAdmin
162
+ .from("contacts")
163
+ .delete()
164
+ .eq("id", parseInt(contactId, 10));
165
+
166
+ if (error) {
167
+ return createErrorResponse(404, "Contact not found");
168
+ }
169
+
170
+ return new Response(null, { status: 204, headers: corsHeaders });
171
+ }
@@ -0,0 +1,166 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
3
+ import { corsHeaders, createErrorResponse } from "../_shared/utils.ts";
4
+ import {
5
+ validateApiKey,
6
+ checkRateLimit,
7
+ hasScope,
8
+ logApiRequest,
9
+ } from "../_shared/apiKeyAuth.ts";
10
+
11
+ Deno.serve(async (req: Request) => {
12
+ const startTime = Date.now();
13
+
14
+ if (req.method === "OPTIONS") {
15
+ return new Response(null, { status: 204, headers: corsHeaders });
16
+ }
17
+
18
+ const authResult = await validateApiKey(req);
19
+ if ("status" in authResult) {
20
+ return authResult;
21
+ }
22
+ const { apiKey } = authResult;
23
+
24
+ const rateLimitError = checkRateLimit(apiKey.id);
25
+ if (rateLimitError) {
26
+ await logApiRequest(
27
+ apiKey.id,
28
+ "/v1/deals",
29
+ req.method,
30
+ 429,
31
+ Date.now() - startTime,
32
+ req
33
+ );
34
+ return rateLimitError;
35
+ }
36
+
37
+ try {
38
+ const url = new URL(req.url);
39
+ const pathParts = url.pathname.split("/").filter(Boolean);
40
+ const dealId = pathParts[1];
41
+
42
+ let response: Response;
43
+
44
+ if (req.method === "GET" && dealId) {
45
+ response = await getDeal(apiKey, dealId);
46
+ } else if (req.method === "POST") {
47
+ response = await createDeal(apiKey, req);
48
+ } else if (req.method === "PATCH" && dealId) {
49
+ response = await updateDeal(apiKey, dealId, req);
50
+ } else if (req.method === "DELETE" && dealId) {
51
+ response = await deleteDeal(apiKey, dealId);
52
+ } else {
53
+ response = createErrorResponse(404, "Not found");
54
+ }
55
+
56
+ const responseTime = Date.now() - startTime;
57
+ await logApiRequest(
58
+ apiKey.id,
59
+ url.pathname,
60
+ req.method,
61
+ response.status,
62
+ responseTime,
63
+ req
64
+ );
65
+
66
+ return response;
67
+ } catch (error) {
68
+ const responseTime = Date.now() - startTime;
69
+ await logApiRequest(
70
+ apiKey.id,
71
+ new URL(req.url).pathname,
72
+ req.method,
73
+ 500,
74
+ responseTime,
75
+ req,
76
+ error.message
77
+ );
78
+ return createErrorResponse(500, "Internal server error");
79
+ }
80
+ });
81
+
82
+ async function getDeal(apiKey: any, dealId: string) {
83
+ if (!hasScope(apiKey, "deals:read")) {
84
+ return createErrorResponse(403, "Insufficient permissions");
85
+ }
86
+
87
+ const { data, error } = await supabaseAdmin
88
+ .from("deals")
89
+ .select("*")
90
+ .eq("id", dealId)
91
+ .single();
92
+
93
+ if (error || !data) {
94
+ return createErrorResponse(404, "Deal not found");
95
+ }
96
+
97
+ return new Response(JSON.stringify({ data }), {
98
+ headers: { "Content-Type": "application/json", ...corsHeaders },
99
+ });
100
+ }
101
+
102
+ async function createDeal(apiKey: any, req: Request) {
103
+ if (!hasScope(apiKey, "deals:write")) {
104
+ return createErrorResponse(403, "Insufficient permissions");
105
+ }
106
+
107
+ const body = await req.json();
108
+
109
+ const { data, error } = await supabaseAdmin
110
+ .from("deals")
111
+ .insert({
112
+ ...body,
113
+ sales_id: apiKey.sales_id,
114
+ })
115
+ .select()
116
+ .single();
117
+
118
+ if (error) {
119
+ return createErrorResponse(400, error.message);
120
+ }
121
+
122
+ return new Response(JSON.stringify({ data }), {
123
+ status: 201,
124
+ headers: { "Content-Type": "application/json", ...corsHeaders },
125
+ });
126
+ }
127
+
128
+ async function updateDeal(apiKey: any, dealId: string, req: Request) {
129
+ if (!hasScope(apiKey, "deals:write")) {
130
+ return createErrorResponse(403, "Insufficient permissions");
131
+ }
132
+
133
+ const body = await req.json();
134
+
135
+ const { data, error } = await supabaseAdmin
136
+ .from("deals")
137
+ .update(body)
138
+ .eq("id", dealId)
139
+ .select()
140
+ .single();
141
+
142
+ if (error || !data) {
143
+ return createErrorResponse(404, "Deal not found");
144
+ }
145
+
146
+ return new Response(JSON.stringify({ data }), {
147
+ headers: { "Content-Type": "application/json", ...corsHeaders },
148
+ });
149
+ }
150
+
151
+ async function deleteDeal(apiKey: any, dealId: string) {
152
+ if (!hasScope(apiKey, "deals:write")) {
153
+ return createErrorResponse(403, "Insufficient permissions");
154
+ }
155
+
156
+ const { error } = await supabaseAdmin
157
+ .from("deals")
158
+ .delete()
159
+ .eq("id", dealId);
160
+
161
+ if (error) {
162
+ return createErrorResponse(404, "Deal not found");
163
+ }
164
+
165
+ return new Response(null, { status: 204, headers: corsHeaders });
166
+ }
@@ -0,0 +1,133 @@
1
+ import "jsr:@supabase/functions-js/edge-runtime.d.ts";
2
+ import { supabaseAdmin } from "../_shared/supabaseAdmin.ts";
3
+ import { generateWebhookSignature } from "../_shared/webhookSignature.ts";
4
+
5
+ Deno.serve(async () => {
6
+ console.log("Webhook dispatcher running...");
7
+
8
+ // Get pending webhook deliveries
9
+ const { data: queueItems } = await supabaseAdmin
10
+ .from("webhook_queue")
11
+ .select("*, webhooks(*)")
12
+ .eq("status", "pending")
13
+ .lte("next_retry_at", new Date().toISOString())
14
+ .limit(50);
15
+
16
+ if (!queueItems || queueItems.length === 0) {
17
+ return new Response(JSON.stringify({ processed: 0 }), {
18
+ headers: { "Content-Type": "application/json" },
19
+ });
20
+ }
21
+
22
+ console.log(`Processing ${queueItems.length} webhook deliveries`);
23
+
24
+ const results = await Promise.allSettled(
25
+ queueItems.map((item) => deliverWebhook(item))
26
+ );
27
+
28
+ const successful = results.filter((r) => r.status === "fulfilled").length;
29
+ const failed = results.filter((r) => r.status === "rejected").length;
30
+
31
+ return new Response(
32
+ JSON.stringify({ processed: queueItems.length, successful, failed }),
33
+ { headers: { "Content-Type": "application/json" } }
34
+ );
35
+ });
36
+
37
+ async function deliverWebhook(queueItem: any) {
38
+ const webhook = queueItem.webhooks;
39
+
40
+ // Mark as processing
41
+ await supabaseAdmin
42
+ .from("webhook_queue")
43
+ .update({ status: "processing" })
44
+ .eq("id", queueItem.id);
45
+
46
+ try {
47
+ const payloadString = JSON.stringify(queueItem.payload);
48
+ const signature = await generateWebhookSignature(
49
+ webhook.secret,
50
+ payloadString
51
+ );
52
+
53
+ const headers: Record<string, string> = {
54
+ "Content-Type": "application/json",
55
+ "X-Webhook-Signature": signature,
56
+ "X-Webhook-Event": queueItem.event_type,
57
+ "User-Agent": "AtomicCRM-Webhooks/1.0",
58
+ };
59
+
60
+ // Add custom headers from webhook config
61
+ if (webhook.headers) {
62
+ Object.assign(headers, webhook.headers);
63
+ }
64
+
65
+ const response = await fetch(webhook.url, {
66
+ method: "POST",
67
+ headers,
68
+ body: payloadString,
69
+ signal: AbortSignal.timeout(30000), // 30 second timeout
70
+ });
71
+
72
+ if (response.ok) {
73
+ // Success
74
+ await supabaseAdmin
75
+ .from("webhook_queue")
76
+ .update({
77
+ status: "delivered",
78
+ delivered_at: new Date().toISOString(),
79
+ })
80
+ .eq("id", queueItem.id);
81
+
82
+ await supabaseAdmin
83
+ .from("webhooks")
84
+ .update({
85
+ last_triggered_at: new Date().toISOString(),
86
+ failure_count: 0,
87
+ })
88
+ .eq("id", webhook.id);
89
+
90
+ console.log(`Webhook ${queueItem.id} delivered successfully`);
91
+ } else {
92
+ throw new Error(`HTTP ${response.status}: ${await response.text()}`);
93
+ }
94
+ } catch (error) {
95
+ console.error(`Webhook ${queueItem.id} delivery failed:`, error);
96
+
97
+ const newAttempts = queueItem.attempts + 1;
98
+ const shouldRetry = newAttempts < queueItem.max_attempts;
99
+
100
+ if (shouldRetry) {
101
+ // Exponential backoff: 1min, 5min, 15min
102
+ const retryDelays = [60, 300, 900];
103
+ const delaySeconds = retryDelays[newAttempts - 1] || 900;
104
+ const nextRetry = new Date(Date.now() + delaySeconds * 1000)
105
+ .toISOString();
106
+
107
+ await supabaseAdmin
108
+ .from("webhook_queue")
109
+ .update({
110
+ status: "pending",
111
+ attempts: newAttempts,
112
+ next_retry_at: nextRetry,
113
+ error_message: error.message,
114
+ })
115
+ .eq("id", queueItem.id);
116
+ } else {
117
+ // Max attempts reached
118
+ await supabaseAdmin
119
+ .from("webhook_queue")
120
+ .update({
121
+ status: "failed",
122
+ attempts: newAttempts,
123
+ error_message: error.message,
124
+ })
125
+ .eq("id", queueItem.id);
126
+
127
+ await supabaseAdmin
128
+ .from("webhooks")
129
+ .update({ failure_count: webhook.failure_count + 1 })
130
+ .eq("id", webhook.id);
131
+ }
132
+ }
133
+ }
@@ -0,0 +1,133 @@
1
+ -- API Keys table
2
+ create table "public"."api_keys" (
3
+ "id" bigint generated by default as identity not null,
4
+ "created_at" timestamp with time zone not null default now(),
5
+ "sales_id" bigint not null,
6
+ "name" text not null,
7
+ "key_hash" text not null,
8
+ "key_prefix" text not null,
9
+ "scopes" text[] not null default '{}',
10
+ "is_active" boolean not null default true,
11
+ "expires_at" timestamp with time zone,
12
+ "last_used_at" timestamp with time zone,
13
+ "created_by_sales_id" bigint not null
14
+ );
15
+
16
+ alter table "public"."api_keys" enable row level security;
17
+
18
+ CREATE UNIQUE INDEX api_keys_pkey ON public.api_keys USING btree (id);
19
+ CREATE UNIQUE INDEX api_keys_key_hash_unique ON public.api_keys USING btree (key_hash);
20
+ CREATE INDEX api_keys_sales_id_idx ON public.api_keys USING btree (sales_id);
21
+
22
+ alter table "public"."api_keys" add constraint "api_keys_pkey" PRIMARY KEY using index "api_keys_pkey";
23
+ alter table "public"."api_keys" add constraint "api_keys_sales_id_fkey"
24
+ FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
25
+ alter table "public"."api_keys" validate constraint "api_keys_sales_id_fkey";
26
+ alter table "public"."api_keys" add constraint "api_keys_created_by_sales_id_fkey"
27
+ FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
28
+ alter table "public"."api_keys" validate constraint "api_keys_created_by_sales_id_fkey";
29
+
30
+ -- Webhooks table
31
+ create table "public"."webhooks" (
32
+ "id" bigint generated by default as identity not null,
33
+ "created_at" timestamp with time zone not null default now(),
34
+ "sales_id" bigint not null,
35
+ "name" text not null,
36
+ "url" text not null,
37
+ "events" text[] not null default '{}',
38
+ "headers" jsonb,
39
+ "is_active" boolean not null default true,
40
+ "secret" text not null,
41
+ "last_triggered_at" timestamp with time zone,
42
+ "failure_count" int not null default 0,
43
+ "created_by_sales_id" bigint not null
44
+ );
45
+
46
+ alter table "public"."webhooks" enable row level security;
47
+
48
+ CREATE UNIQUE INDEX webhooks_pkey ON public.webhooks USING btree (id);
49
+ CREATE INDEX webhooks_sales_id_idx ON public.webhooks USING btree (sales_id);
50
+ CREATE INDEX webhooks_events_idx ON public.webhooks USING gin (events);
51
+
52
+ alter table "public"."webhooks" add constraint "webhooks_pkey" PRIMARY KEY using index "webhooks_pkey";
53
+ alter table "public"."webhooks" add constraint "webhooks_sales_id_fkey"
54
+ FOREIGN KEY (sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
55
+ alter table "public"."webhooks" validate constraint "webhooks_sales_id_fkey";
56
+ alter table "public"."webhooks" add constraint "webhooks_created_by_sales_id_fkey"
57
+ FOREIGN KEY (created_by_sales_id) REFERENCES sales(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
58
+ alter table "public"."webhooks" validate constraint "webhooks_created_by_sales_id_fkey";
59
+
60
+ -- API Request Logs table
61
+ create table "public"."api_logs" (
62
+ "id" bigint generated by default as identity not null,
63
+ "created_at" timestamp with time zone not null default now(),
64
+ "api_key_id" bigint not null,
65
+ "endpoint" text not null,
66
+ "method" text not null,
67
+ "status_code" int not null,
68
+ "response_time_ms" int,
69
+ "ip_address" text,
70
+ "user_agent" text,
71
+ "error_message" text
72
+ );
73
+
74
+ alter table "public"."api_logs" enable row level security;
75
+
76
+ CREATE UNIQUE INDEX api_logs_pkey ON public.api_logs USING btree (id);
77
+ CREATE INDEX api_logs_api_key_id_idx ON public.api_logs USING btree (api_key_id);
78
+ CREATE INDEX api_logs_created_at_idx ON public.api_logs USING btree (created_at DESC);
79
+
80
+ alter table "public"."api_logs" add constraint "api_logs_pkey" PRIMARY KEY using index "api_logs_pkey";
81
+ alter table "public"."api_logs" add constraint "api_logs_api_key_id_fkey"
82
+ FOREIGN KEY (api_key_id) REFERENCES api_keys(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
83
+ alter table "public"."api_logs" validate constraint "api_logs_api_key_id_fkey";
84
+
85
+ -- Webhook Queue table (for async delivery)
86
+ create table "public"."webhook_queue" (
87
+ "id" bigint generated by default as identity not null,
88
+ "created_at" timestamp with time zone not null default now(),
89
+ "webhook_id" bigint not null,
90
+ "event_type" text not null,
91
+ "payload" jsonb not null,
92
+ "status" text not null default 'pending',
93
+ "attempts" int not null default 0,
94
+ "max_attempts" int not null default 3,
95
+ "next_retry_at" timestamp with time zone,
96
+ "delivered_at" timestamp with time zone,
97
+ "error_message" text
98
+ );
99
+
100
+ alter table "public"."webhook_queue" enable row level security;
101
+
102
+ CREATE UNIQUE INDEX webhook_queue_pkey ON public.webhook_queue USING btree (id);
103
+ CREATE INDEX webhook_queue_status_idx ON public.webhook_queue USING btree (status, next_retry_at);
104
+ CREATE INDEX webhook_queue_webhook_id_idx ON public.webhook_queue USING btree (webhook_id);
105
+
106
+ alter table "public"."webhook_queue" add constraint "webhook_queue_pkey" PRIMARY KEY using index "webhook_queue_pkey";
107
+ alter table "public"."webhook_queue" add constraint "webhook_queue_webhook_id_fkey"
108
+ FOREIGN KEY (webhook_id) REFERENCES webhooks(id) ON UPDATE CASCADE ON DELETE CASCADE not valid;
109
+ alter table "public"."webhook_queue" validate constraint "webhook_queue_webhook_id_fkey";
110
+
111
+ -- Grant permissions
112
+ grant select, insert, update, delete on table "public"."api_keys" to "authenticated";
113
+ grant select, insert, update, delete on table "public"."webhooks" to "authenticated";
114
+ grant select on table "public"."api_logs" to "authenticated";
115
+ grant select on table "public"."webhook_queue" to "authenticated";
116
+
117
+ grant all on table "public"."api_keys" to "service_role";
118
+ grant all on table "public"."webhooks" to "service_role";
119
+ grant all on table "public"."api_logs" to "service_role";
120
+ grant all on table "public"."webhook_queue" to "service_role";
121
+
122
+ -- RLS Policies (permissive for authenticated users)
123
+ create policy "Enable all for authenticated users" on "public"."api_keys"
124
+ for all to authenticated using (true) with check (true);
125
+
126
+ create policy "Enable all for authenticated users" on "public"."webhooks"
127
+ for all to authenticated using (true) with check (true);
128
+
129
+ create policy "Enable read for authenticated users" on "public"."api_logs"
130
+ for select to authenticated using (true);
131
+
132
+ create policy "Enable read for authenticated users" on "public"."webhook_queue"
133
+ for select to authenticated using (true);