realtimex-crm 0.8.1 → 0.9.2

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 (55) hide show
  1. package/bin/realtimex-crm.js +56 -32
  2. package/dist/assets/{DealList-B3alafQv.js → DealList-DbwJCRGl.js} +2 -2
  3. package/dist/assets/{DealList-B3alafQv.js.map → DealList-DbwJCRGl.js.map} +1 -1
  4. package/dist/assets/index-C__S90Gb.css +1 -0
  5. package/dist/assets/index-mE-upBfc.js +166 -0
  6. package/dist/assets/index-mE-upBfc.js.map +1 -0
  7. package/dist/index.html +1 -1
  8. package/dist/stats.html +1 -1
  9. package/package.json +3 -1
  10. package/src/components/atomic-crm/activities/ActivitiesPage.tsx +16 -0
  11. package/src/components/atomic-crm/activities/ActivityFeed.tsx +212 -0
  12. package/src/components/atomic-crm/activities/FileUpload.tsx +359 -0
  13. package/src/components/atomic-crm/contacts/ContactShow.tsx +28 -10
  14. package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
  15. package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
  16. package/src/components/atomic-crm/integrations/CreateChannelDialog.tsx +139 -0
  17. package/src/components/atomic-crm/integrations/IngestionChannelsTab.tsx +188 -0
  18. package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +46 -0
  19. package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
  20. package/src/components/atomic-crm/layout/Header.tsx +14 -1
  21. package/src/components/atomic-crm/root/CRM.tsx +2 -0
  22. package/src/components/ui/alert-dialog.tsx +155 -0
  23. package/src/lib/api-key-utils.ts +22 -0
  24. package/supabase/fix_webhook_hardcoded.sql +34 -0
  25. package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
  26. package/supabase/functions/_shared/ingestionGuard.ts +128 -0
  27. package/supabase/functions/_shared/utils.ts +1 -1
  28. package/supabase/functions/_shared/webhookSignature.ts +23 -0
  29. package/supabase/functions/api-v1-activities/index.ts +137 -0
  30. package/supabase/functions/api-v1-companies/index.ts +166 -0
  31. package/supabase/functions/api-v1-contacts/index.ts +171 -0
  32. package/supabase/functions/api-v1-deals/index.ts +166 -0
  33. package/supabase/functions/ingest-activity/.well-known/supabase/config.toml +4 -0
  34. package/supabase/functions/ingest-activity/index.ts +261 -0
  35. package/supabase/functions/webhook-dispatcher/index.ts +133 -0
  36. package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
  37. package/supabase/migrations/20251219120100_webhook_triggers.sql +176 -0
  38. package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
  39. package/supabase/migrations/20251220120000_realtime_ingestion.sql +154 -0
  40. package/supabase/migrations/20251221000000_contact_matching.sql +94 -0
  41. package/supabase/migrations/20251221000001_fix_ingestion_providers_rls.sql +23 -0
  42. package/supabase/migrations/20251221000002_fix_contact_matching_jsonb.sql +67 -0
  43. package/supabase/migrations/20251221000003_fix_email_matching.sql +70 -0
  44. package/supabase/migrations/20251221000004_time_based_work_stealing.sql +99 -0
  45. package/supabase/migrations/20251221000005_realtime_functions.sql +73 -0
  46. package/supabase/migrations/20251221000006_enable_pg_net.sql +3 -0
  47. package/supabase/migrations/20251222075019_enable_extensions_and_configure_cron.sql +86 -0
  48. package/supabase/migrations/20251222094036_large_payload_storage.sql +213 -0
  49. package/supabase/migrations/20251222094247_large_payload_cron.sql +28 -0
  50. package/supabase/migrations/20251222220000_fix_large_payload_types.sql +72 -0
  51. package/supabase/migrations/20251223000000_enable_realtime_all_crm_tables.sql +50 -0
  52. package/supabase/migrations/20251223185638_remove_large_payload_storage.sql +54 -0
  53. package/dist/assets/index-CHk72bAf.css +0 -1
  54. package/dist/assets/index-mhAjQ1_w.js +0 -153
  55. package/dist/assets/index-mhAjQ1_w.js.map +0 -1
@@ -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
+ }
@@ -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,4 @@
1
+ # Disable JWT verification for this function
2
+ # Webhooks from Twilio, Postmark, etc. don't have Supabase JWTs
3
+ # Security is handled via ingestion_key parameter and provider-specific signatures
4
+ verify_jwt = false