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,155 @@
1
+ import * as React from "react"
2
+ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
3
+
4
+ import { cn } from "@/lib/utils"
5
+ import { buttonVariants } from "@/components/ui/button"
6
+
7
+ function AlertDialog({
8
+ ...props
9
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Root>) {
10
+ return <AlertDialogPrimitive.Root data-slot="alert-dialog" {...props} />
11
+ }
12
+
13
+ function AlertDialogTrigger({
14
+ ...props
15
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Trigger>) {
16
+ return (
17
+ <AlertDialogPrimitive.Trigger data-slot="alert-dialog-trigger" {...props} />
18
+ )
19
+ }
20
+
21
+ function AlertDialogPortal({
22
+ ...props
23
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Portal>) {
24
+ return (
25
+ <AlertDialogPrimitive.Portal data-slot="alert-dialog-portal" {...props} />
26
+ )
27
+ }
28
+
29
+ function AlertDialogOverlay({
30
+ className,
31
+ ...props
32
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Overlay>) {
33
+ return (
34
+ <AlertDialogPrimitive.Overlay
35
+ data-slot="alert-dialog-overlay"
36
+ className={cn(
37
+ "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
38
+ className
39
+ )}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ function AlertDialogContent({
46
+ className,
47
+ ...props
48
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Content>) {
49
+ return (
50
+ <AlertDialogPortal>
51
+ <AlertDialogOverlay />
52
+ <AlertDialogPrimitive.Content
53
+ data-slot="alert-dialog-content"
54
+ className={cn(
55
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
56
+ className
57
+ )}
58
+ {...props}
59
+ />
60
+ </AlertDialogPortal>
61
+ )
62
+ }
63
+
64
+ function AlertDialogHeader({
65
+ className,
66
+ ...props
67
+ }: React.ComponentProps<"div">) {
68
+ return (
69
+ <div
70
+ data-slot="alert-dialog-header"
71
+ className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
72
+ {...props}
73
+ />
74
+ )
75
+ }
76
+
77
+ function AlertDialogFooter({
78
+ className,
79
+ ...props
80
+ }: React.ComponentProps<"div">) {
81
+ return (
82
+ <div
83
+ data-slot="alert-dialog-footer"
84
+ className={cn(
85
+ "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
86
+ className
87
+ )}
88
+ {...props}
89
+ />
90
+ )
91
+ }
92
+
93
+ function AlertDialogTitle({
94
+ className,
95
+ ...props
96
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Title>) {
97
+ return (
98
+ <AlertDialogPrimitive.Title
99
+ data-slot="alert-dialog-title"
100
+ className={cn("text-lg font-semibold", className)}
101
+ {...props}
102
+ />
103
+ )
104
+ }
105
+
106
+ function AlertDialogDescription({
107
+ className,
108
+ ...props
109
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Description>) {
110
+ return (
111
+ <AlertDialogPrimitive.Description
112
+ data-slot="alert-dialog-description"
113
+ className={cn("text-muted-foreground text-sm", className)}
114
+ {...props}
115
+ />
116
+ )
117
+ }
118
+
119
+ function AlertDialogAction({
120
+ className,
121
+ ...props
122
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Action>) {
123
+ return (
124
+ <AlertDialogPrimitive.Action
125
+ className={cn(buttonVariants(), className)}
126
+ {...props}
127
+ />
128
+ )
129
+ }
130
+
131
+ function AlertDialogCancel({
132
+ className,
133
+ ...props
134
+ }: React.ComponentProps<typeof AlertDialogPrimitive.Cancel>) {
135
+ return (
136
+ <AlertDialogPrimitive.Cancel
137
+ className={cn(buttonVariants({ variant: "outline" }), className)}
138
+ {...props}
139
+ />
140
+ )
141
+ }
142
+
143
+ export {
144
+ AlertDialog,
145
+ AlertDialogPortal,
146
+ AlertDialogOverlay,
147
+ AlertDialogTrigger,
148
+ AlertDialogContent,
149
+ AlertDialogHeader,
150
+ AlertDialogFooter,
151
+ AlertDialogTitle,
152
+ AlertDialogDescription,
153
+ AlertDialogAction,
154
+ AlertDialogCancel,
155
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Generate a new API key in format: ak_live_<32 hex chars>
3
+ */
4
+ export function generateApiKey(): string {
5
+ const array = new Uint8Array(16);
6
+ crypto.getRandomValues(array);
7
+ const hex = Array.from(array)
8
+ .map((b) => b.toString(16).padStart(2, "0"))
9
+ .join("");
10
+ return `ak_live_${hex}`;
11
+ }
12
+
13
+ /**
14
+ * Hash API key using SHA-256
15
+ */
16
+ export async function hashApiKey(apiKey: string): Promise<string> {
17
+ const encoder = new TextEncoder();
18
+ const data = encoder.encode(apiKey);
19
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
20
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
21
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
22
+ }
@@ -0,0 +1,171 @@
1
+ import { supabaseAdmin } from "./supabaseAdmin.ts";
2
+ import { createErrorResponse } from "./utils.ts";
3
+
4
+ // Rate limiting: Simple in-memory store (resets on function restart)
5
+ const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
6
+
7
+ const RATE_LIMIT_MAX = 100; // requests per window
8
+ const RATE_LIMIT_WINDOW = 60 * 1000; // 60 seconds
9
+
10
+ interface ApiKey {
11
+ id: number;
12
+ sales_id: number;
13
+ name: string;
14
+ scopes: string[];
15
+ is_active: boolean;
16
+ expires_at: string | null;
17
+ }
18
+
19
+ /**
20
+ * Extract and validate API key from request
21
+ * Returns api key record or error response
22
+ */
23
+ export async function validateApiKey(
24
+ req: Request
25
+ ): Promise<{ apiKey: ApiKey } | Response> {
26
+ const authHeader = req.headers.get("Authorization");
27
+
28
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
29
+ return createErrorResponse(401, "Missing or invalid Authorization header");
30
+ }
31
+
32
+ const apiKeyValue = authHeader.replace("Bearer ", "");
33
+
34
+ if (!apiKeyValue.startsWith("ak_live_")) {
35
+ return createErrorResponse(401, "Invalid API key format");
36
+ }
37
+
38
+ // Hash the API key for lookup
39
+ const encoder = new TextEncoder();
40
+ const data = encoder.encode(apiKeyValue);
41
+ const hashBuffer = await crypto.subtle.digest("SHA-256", data);
42
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
43
+ const keyHash = hashArray.map((b) => b.toString(16).padStart(2, "0")).join(
44
+ ""
45
+ );
46
+
47
+ // Lookup API key in database
48
+ const { data: apiKey, error } = await supabaseAdmin
49
+ .from("api_keys")
50
+ .select("id, sales_id, name, scopes, is_active, expires_at")
51
+ .eq("key_hash", keyHash)
52
+ .single();
53
+
54
+ if (error || !apiKey) {
55
+ return createErrorResponse(401, "Invalid API key");
56
+ }
57
+
58
+ // Check if active
59
+ if (!apiKey.is_active) {
60
+ return createErrorResponse(401, "API key is disabled");
61
+ }
62
+
63
+ // Check expiration
64
+ if (apiKey.expires_at && new Date(apiKey.expires_at) < new Date()) {
65
+ return createErrorResponse(401, "API key has expired");
66
+ }
67
+
68
+ // Update last_used_at (fire and forget)
69
+ supabaseAdmin
70
+ .from("api_keys")
71
+ .update({ last_used_at: new Date().toISOString() })
72
+ .eq("id", apiKey.id)
73
+ .then(() => {});
74
+
75
+ return { apiKey };
76
+ }
77
+
78
+ /**
79
+ * Check rate limit for API key
80
+ */
81
+ export function checkRateLimit(apiKeyId: number): Response | null {
82
+ const now = Date.now();
83
+ const key = `ratelimit:${apiKeyId}`;
84
+
85
+ let bucket = rateLimitStore.get(key);
86
+
87
+ if (!bucket || now > bucket.resetAt) {
88
+ // Create new bucket
89
+ bucket = {
90
+ count: 1,
91
+ resetAt: now + RATE_LIMIT_WINDOW,
92
+ };
93
+ rateLimitStore.set(key, bucket);
94
+ return null;
95
+ }
96
+
97
+ if (bucket.count >= RATE_LIMIT_MAX) {
98
+ const retryAfter = Math.ceil((bucket.resetAt - now) / 1000);
99
+ return new Response(
100
+ JSON.stringify({
101
+ status: 429,
102
+ message: "Rate limit exceeded",
103
+ retry_after: retryAfter,
104
+ }),
105
+ {
106
+ status: 429,
107
+ headers: {
108
+ "Content-Type": "application/json",
109
+ "Retry-After": retryAfter.toString(),
110
+ "X-RateLimit-Limit": RATE_LIMIT_MAX.toString(),
111
+ "X-RateLimit-Remaining": "0",
112
+ "X-RateLimit-Reset": bucket.resetAt.toString(),
113
+ },
114
+ }
115
+ );
116
+ }
117
+
118
+ bucket.count++;
119
+
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Check if API key has required scope
125
+ */
126
+ export function hasScope(apiKey: ApiKey, requiredScope: string): boolean {
127
+ // Check for wildcard scope
128
+ if (apiKey.scopes.includes("*")) {
129
+ return true;
130
+ }
131
+
132
+ // Check for exact scope match
133
+ if (apiKey.scopes.includes(requiredScope)) {
134
+ return true;
135
+ }
136
+
137
+ // Check for resource wildcard (e.g., "contacts:*" matches "contacts:read")
138
+ const [resource] = requiredScope.split(":");
139
+ if (apiKey.scopes.includes(`${resource}:*`)) {
140
+ return true;
141
+ }
142
+
143
+ return false;
144
+ }
145
+
146
+ /**
147
+ * Log API request
148
+ */
149
+ export async function logApiRequest(
150
+ apiKeyId: number,
151
+ endpoint: string,
152
+ method: string,
153
+ statusCode: number,
154
+ responseTimeMs: number,
155
+ req: Request,
156
+ errorMessage?: string
157
+ ) {
158
+ const ip = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim();
159
+ const userAgent = req.headers.get("user-agent");
160
+
161
+ await supabaseAdmin.from("api_logs").insert({
162
+ api_key_id: apiKeyId,
163
+ endpoint,
164
+ method,
165
+ status_code: statusCode,
166
+ response_time_ms: responseTimeMs,
167
+ ip_address: ip,
168
+ user_agent: userAgent,
169
+ error_message: errorMessage,
170
+ });
171
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Generate HMAC-SHA256 signature for webhook payload
3
+ */
4
+ export async function generateWebhookSignature(
5
+ secret: string,
6
+ payload: string
7
+ ): Promise<string> {
8
+ const encoder = new TextEncoder();
9
+ const keyData = encoder.encode(secret);
10
+ const messageData = encoder.encode(payload);
11
+
12
+ const cryptoKey = await crypto.subtle.importKey(
13
+ "raw",
14
+ keyData,
15
+ { name: "HMAC", hash: "SHA-256" },
16
+ false,
17
+ ["sign"]
18
+ );
19
+
20
+ const signature = await crypto.subtle.sign("HMAC", cryptoKey, messageData);
21
+ const hashArray = Array.from(new Uint8Array(signature));
22
+ return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
23
+ }
@@ -0,0 +1,137 @@
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/activities",
29
+ req.method,
30
+ 429,
31
+ Date.now() - startTime,
32
+ req
33
+ );
34
+ return rateLimitError;
35
+ }
36
+
37
+ try {
38
+ if (req.method === "POST") {
39
+ const response = await createActivity(apiKey, req);
40
+ const responseTime = Date.now() - startTime;
41
+ await logApiRequest(
42
+ apiKey.id,
43
+ "/v1/activities",
44
+ req.method,
45
+ response.status,
46
+ responseTime,
47
+ req
48
+ );
49
+ return response;
50
+ } else {
51
+ return createErrorResponse(404, "Not found");
52
+ }
53
+ } catch (error) {
54
+ const responseTime = Date.now() - startTime;
55
+ await logApiRequest(
56
+ apiKey.id,
57
+ "/v1/activities",
58
+ req.method,
59
+ 500,
60
+ responseTime,
61
+ req,
62
+ error.message
63
+ );
64
+ return createErrorResponse(500, "Internal server error");
65
+ }
66
+ });
67
+
68
+ async function createActivity(apiKey: any, req: Request) {
69
+ if (!hasScope(apiKey, "activities:write")) {
70
+ return createErrorResponse(403, "Insufficient permissions");
71
+ }
72
+
73
+ const body = await req.json();
74
+ const { type, ...activityData } = body;
75
+
76
+ // Activities can be notes or tasks
77
+ if (type === "note" || type === "contact_note") {
78
+ const { data, error } = await supabaseAdmin
79
+ .from("contactNotes")
80
+ .insert({
81
+ ...activityData,
82
+ sales_id: apiKey.sales_id,
83
+ })
84
+ .select()
85
+ .single();
86
+
87
+ if (error) {
88
+ return createErrorResponse(400, error.message);
89
+ }
90
+
91
+ return new Response(JSON.stringify({ data, type: "note" }), {
92
+ status: 201,
93
+ headers: { "Content-Type": "application/json", ...corsHeaders },
94
+ });
95
+ } else if (type === "task") {
96
+ const { data, error } = await supabaseAdmin
97
+ .from("tasks")
98
+ .insert({
99
+ ...activityData,
100
+ sales_id: apiKey.sales_id,
101
+ })
102
+ .select()
103
+ .single();
104
+
105
+ if (error) {
106
+ return createErrorResponse(400, error.message);
107
+ }
108
+
109
+ return new Response(JSON.stringify({ data, type: "task" }), {
110
+ status: 201,
111
+ headers: { "Content-Type": "application/json", ...corsHeaders },
112
+ });
113
+ } else if (type === "deal_note") {
114
+ const { data, error } = await supabaseAdmin
115
+ .from("dealNotes")
116
+ .insert({
117
+ ...activityData,
118
+ sales_id: apiKey.sales_id,
119
+ })
120
+ .select()
121
+ .single();
122
+
123
+ if (error) {
124
+ return createErrorResponse(400, error.message);
125
+ }
126
+
127
+ return new Response(JSON.stringify({ data, type: "deal_note" }), {
128
+ status: 201,
129
+ headers: { "Content-Type": "application/json", ...corsHeaders },
130
+ });
131
+ } else {
132
+ return createErrorResponse(
133
+ 400,
134
+ "Invalid activity type. Must be 'note', 'contact_note', 'task', or 'deal_note'"
135
+ );
136
+ }
137
+ }
@@ -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/companies",
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 companyId = pathParts[1];
41
+
42
+ let response: Response;
43
+
44
+ if (req.method === "GET" && companyId) {
45
+ response = await getCompany(apiKey, companyId);
46
+ } else if (req.method === "POST") {
47
+ response = await createCompany(apiKey, req);
48
+ } else if (req.method === "PATCH" && companyId) {
49
+ response = await updateCompany(apiKey, companyId, req);
50
+ } else if (req.method === "DELETE" && companyId) {
51
+ response = await deleteCompany(apiKey, companyId);
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 getCompany(apiKey: any, companyId: string) {
83
+ if (!hasScope(apiKey, "companies:read")) {
84
+ return createErrorResponse(403, "Insufficient permissions");
85
+ }
86
+
87
+ const { data, error } = await supabaseAdmin
88
+ .from("companies")
89
+ .select("*")
90
+ .eq("id", companyId)
91
+ .single();
92
+
93
+ if (error || !data) {
94
+ return createErrorResponse(404, "Company not found");
95
+ }
96
+
97
+ return new Response(JSON.stringify({ data }), {
98
+ headers: { "Content-Type": "application/json", ...corsHeaders },
99
+ });
100
+ }
101
+
102
+ async function createCompany(apiKey: any, req: Request) {
103
+ if (!hasScope(apiKey, "companies:write")) {
104
+ return createErrorResponse(403, "Insufficient permissions");
105
+ }
106
+
107
+ const body = await req.json();
108
+
109
+ const { data, error } = await supabaseAdmin
110
+ .from("companies")
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 updateCompany(apiKey: any, companyId: string, req: Request) {
129
+ if (!hasScope(apiKey, "companies:write")) {
130
+ return createErrorResponse(403, "Insufficient permissions");
131
+ }
132
+
133
+ const body = await req.json();
134
+
135
+ const { data, error } = await supabaseAdmin
136
+ .from("companies")
137
+ .update(body)
138
+ .eq("id", companyId)
139
+ .select()
140
+ .single();
141
+
142
+ if (error || !data) {
143
+ return createErrorResponse(404, "Company not found");
144
+ }
145
+
146
+ return new Response(JSON.stringify({ data }), {
147
+ headers: { "Content-Type": "application/json", ...corsHeaders },
148
+ });
149
+ }
150
+
151
+ async function deleteCompany(apiKey: any, companyId: string) {
152
+ if (!hasScope(apiKey, "companies:write")) {
153
+ return createErrorResponse(403, "Insufficient permissions");
154
+ }
155
+
156
+ const { error } = await supabaseAdmin
157
+ .from("companies")
158
+ .delete()
159
+ .eq("id", companyId);
160
+
161
+ if (error) {
162
+ return createErrorResponse(404, "Company not found");
163
+ }
164
+
165
+ return new Response(null, { status: 204, headers: corsHeaders });
166
+ }