@vantageos/vantage-crm-mcp 0.1.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 (48) hide show
  1. package/README.md +260 -0
  2. package/dist/convex/crm/_helpers.js +24 -0
  3. package/dist/convex/crm/activities.js +220 -0
  4. package/dist/convex/crm/briefing.js +198 -0
  5. package/dist/convex/crm/calendarCron.js +92 -0
  6. package/dist/convex/crm/calendarCronDispatch.js +83 -0
  7. package/dist/convex/crm/calendarSync.js +294 -0
  8. package/dist/convex/crm/companies.js +323 -0
  9. package/dist/convex/crm/contacts.js +346 -0
  10. package/dist/convex/crm/deals.js +481 -0
  11. package/dist/convex/crm/emailActions.js +158 -0
  12. package/dist/convex/crm/emailCron.js +210 -0
  13. package/dist/convex/crm/emailCronDispatch.js +76 -0
  14. package/dist/convex/crm/emailSync.js +260 -0
  15. package/dist/convex/crm/onboarding.js +185 -0
  16. package/dist/convex/crm/stats.js +75 -0
  17. package/dist/convex/crm/tasks.js +109 -0
  18. package/dist/convex/crons.js +25 -0
  19. package/dist/convex/integrations.js +183 -0
  20. package/dist/convex/lib/auditLog.js +109 -0
  21. package/dist/convex/lib/auth.js +372 -0
  22. package/dist/convex/lib/rbac.js +123 -0
  23. package/dist/convex/lib/workspace.js +171 -0
  24. package/dist/convex/organizations.js +192 -0
  25. package/dist/convex/schema.js +690 -0
  26. package/dist/convex/users.js +217 -0
  27. package/dist/convex/workspaces.js +603 -0
  28. package/dist/mcp-server/lib/convexClient.js +50 -0
  29. package/dist/mcp-server/lib/scopeEnforcement.js +76 -0
  30. package/dist/mcp-server/registry.js +116 -0
  31. package/dist/mcp-server/server.js +97 -0
  32. package/dist/mcp-server/tests/registry.test.js +163 -0
  33. package/dist/mcp-server/tests/scopeEnforcement.test.js +137 -0
  34. package/dist/mcp-server/tests/security.test.js +257 -0
  35. package/dist/mcp-server/tests/tools.test.js +272 -0
  36. package/dist/mcp-server/tools/activities.js +207 -0
  37. package/dist/mcp-server/tools/admin.js +190 -0
  38. package/dist/mcp-server/tools/companies.js +233 -0
  39. package/dist/mcp-server/tools/contacts.js +306 -0
  40. package/dist/mcp-server/tools/customFields.js +222 -0
  41. package/dist/mcp-server/tools/customObjects.js +235 -0
  42. package/dist/mcp-server/tools/deals.js +297 -0
  43. package/dist/mcp-server/tools/rbac.js +177 -0
  44. package/dist/mcp-server/tools/search.js +155 -0
  45. package/dist/mcp-server/tools/workflows.js +234 -0
  46. package/dist/mcp-server/transport/http.js +257 -0
  47. package/dist/mcp-server/transport/stdio.js +90 -0
  48. package/package.json +45 -0
@@ -0,0 +1,690 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const server_1 = require("convex/server");
4
+ const values_1 = require("convex/values");
5
+ /**
6
+ * VantageCRM Convex Schema — V0.1.0
7
+ *
8
+ * Tables: 21 total
9
+ * KEEP (12): users, organizations, memberships, workspaces, workspaceMembers,
10
+ * contacts, companies, deals, activities, emailSync, calendarEvents, integrations
11
+ * NEW (9): custom_field_definitions, custom_object_definitions, custom_object_records,
12
+ * audit_log, workflow_definitions, workflow_executions,
13
+ * subscriptions, oauth_clients, oauth_access_tokens
14
+ * ARCHIVE(31): moved to convex/_archive/ — not in this schema
15
+ *
16
+ * Multi-tenant isolation: workspaceId mandatory on ALL CRM entities.
17
+ * RBAC: D-003 field-level (workspaceMembers.role + custom_field_definitions.visibility).
18
+ * Soft delete: isArchived + archivedAt on all CRM tables — hard delete admin-only after 30j.
19
+ *
20
+ * Ref: elpi-corp/analysis/vantage-crm-spec-2026-05-20.md (commit ac881d0)
21
+ * elpi-corp/decisions/vantage-crm/architecture-2026-05-20.md (D-001→D-014)
22
+ * Introduced: V0.1.0 (Day 76, 2026-05-20)
23
+ */
24
+ exports.default = (0, server_1.defineSchema)({
25
+ // ============================================================================
26
+ // KEEP — users
27
+ // Role V0.1.0: Mirror Clerk user data. Auth identity source-of-truth.
28
+ // Multi-tenant key: n/a (platform-level identity).
29
+ // Indexes: by_clerk_id, by_token, by_email
30
+ // Introduced: V0.1.0
31
+ // ============================================================================
32
+ users: (0, server_1.defineTable)({
33
+ clerkId: values_1.v.string(),
34
+ tokenIdentifier: values_1.v.string(),
35
+ email: values_1.v.string(),
36
+ emailVerified: values_1.v.optional(values_1.v.boolean()),
37
+ name: values_1.v.optional(values_1.v.string()),
38
+ firstName: values_1.v.optional(values_1.v.string()),
39
+ lastName: values_1.v.optional(values_1.v.string()),
40
+ avatarUrl: values_1.v.optional(values_1.v.string()),
41
+ phone: values_1.v.optional(values_1.v.string()),
42
+ role: values_1.v.string(),
43
+ activeWorkspaceId: values_1.v.optional(values_1.v.id('workspaces')),
44
+ settings: values_1.v.optional(values_1.v.object({
45
+ theme: values_1.v.optional(values_1.v.string()),
46
+ notifications: values_1.v.optional(values_1.v.boolean()),
47
+ })),
48
+ lastLoginAt: values_1.v.optional(values_1.v.number()),
49
+ createdAt: values_1.v.number(),
50
+ updatedAt: values_1.v.number(),
51
+ })
52
+ .index('by_clerk_id', ['clerkId'])
53
+ .index('by_token', ['tokenIdentifier'])
54
+ .index('by_email', ['email']),
55
+ // ============================================================================
56
+ // KEEP — organizations
57
+ // Role V0.1.0: Mirror Clerk Org. Billing subscription anchor (orgId → subscriptions).
58
+ // Multi-tenant key: clerkId (org-level, anchors subscriptions).
59
+ // Indexes: by_clerk_id, by_slug
60
+ // Introduced: V0.1.0
61
+ // ============================================================================
62
+ organizations: (0, server_1.defineTable)({
63
+ clerkId: values_1.v.string(),
64
+ name: values_1.v.string(),
65
+ slug: values_1.v.optional(values_1.v.string()),
66
+ imageUrl: values_1.v.optional(values_1.v.string()),
67
+ plan: values_1.v.optional(values_1.v.string()),
68
+ settings: values_1.v.optional(values_1.v.object({
69
+ defaultModel: values_1.v.optional(values_1.v.string()),
70
+ })),
71
+ memberCount: values_1.v.optional(values_1.v.number()),
72
+ workspaceCount: values_1.v.optional(values_1.v.number()),
73
+ createdAt: values_1.v.number(),
74
+ updatedAt: values_1.v.number(),
75
+ })
76
+ .index('by_clerk_id', ['clerkId'])
77
+ .index('by_slug', ['slug']),
78
+ // ============================================================================
79
+ // KEEP — memberships
80
+ // Role V0.1.0: Clerk org membership mirror. Org-level role (org:admin / org:member).
81
+ // Multi-tenant key: orgId.
82
+ // Indexes: by_user, by_org, by_user_org
83
+ // Introduced: V0.1.0
84
+ // ============================================================================
85
+ memberships: (0, server_1.defineTable)({
86
+ userId: values_1.v.id('users'),
87
+ orgId: values_1.v.id('organizations'),
88
+ clerkUserId: values_1.v.string(),
89
+ clerkOrgId: values_1.v.string(),
90
+ role: values_1.v.string(),
91
+ permissions: values_1.v.optional(values_1.v.object({
92
+ canCreateWorkspaces: values_1.v.optional(values_1.v.boolean()),
93
+ canManageIntegrations: values_1.v.optional(values_1.v.boolean()),
94
+ canInviteMembers: values_1.v.optional(values_1.v.boolean()),
95
+ })),
96
+ joinedAt: values_1.v.number(),
97
+ updatedAt: values_1.v.number(),
98
+ })
99
+ .index('by_user', ['userId'])
100
+ .index('by_org', ['orgId'])
101
+ .index('by_user_org', ['userId', 'orgId']),
102
+ // ============================================================================
103
+ // KEEP — workspaces [EXTENDED V0.1.0: pipelineStages.probability — OQ-2]
104
+ // Role V0.1.0: Tenant boundary effectif. Toute entité CRM porte workspaceId.
105
+ // OQ-2: pipelineStages.probability (0-100) for Salesforce-style auto-set deals.probability.
106
+ // Multi-tenant key: orgId (workspaces belong to orgs).
107
+ // Indexes: by_owner, by_org, by_owner_type, by_slug
108
+ // Introduced: V0.1.0
109
+ // ============================================================================
110
+ workspaces: (0, server_1.defineTable)({
111
+ name: values_1.v.string(),
112
+ slug: values_1.v.optional(values_1.v.string()),
113
+ description: values_1.v.optional(values_1.v.string()),
114
+ icon: values_1.v.optional(values_1.v.string()),
115
+ color: values_1.v.optional(values_1.v.string()),
116
+ ownerType: values_1.v.union(values_1.v.literal('user'), values_1.v.literal('organization')),
117
+ ownerId: values_1.v.id('users'),
118
+ orgId: values_1.v.optional(values_1.v.id('organizations')),
119
+ isDefault: values_1.v.optional(values_1.v.boolean()),
120
+ settings: values_1.v.optional(values_1.v.object({
121
+ calendarSyncEnabled: values_1.v.optional(values_1.v.boolean()),
122
+ icpTags: values_1.v.optional(values_1.v.array(values_1.v.string())),
123
+ })),
124
+ memberCount: values_1.v.optional(values_1.v.number()),
125
+ // OQ-2: probability field added per stage for auto-set Salesforce-style
126
+ pipelineStages: values_1.v.optional(values_1.v.array(values_1.v.object({
127
+ id: values_1.v.string(),
128
+ name: values_1.v.string(),
129
+ order: values_1.v.number(),
130
+ probability: values_1.v.optional(values_1.v.number()), // 0-100, auto-set at moveStage
131
+ }))),
132
+ createdAt: values_1.v.number(),
133
+ updatedAt: values_1.v.number(),
134
+ lastAccessedAt: values_1.v.optional(values_1.v.number()),
135
+ })
136
+ .index('by_owner', ['ownerId'])
137
+ .index('by_org', ['orgId'])
138
+ .index('by_owner_type', ['ownerType', 'ownerId'])
139
+ .index('by_slug', ['slug']),
140
+ // ============================================================================
141
+ // KEEP — workspaceMembers [EXTENDED V0.1.0: CRM permissions (D-003 RBAC)]
142
+ // Role V0.1.0: RBAC workspace-level (owner/admin/editor/viewer). D-003 field-level RBAC.
143
+ // Multi-tenant key: workspaceId.
144
+ // Indexes: by_workspace, by_user, by_workspace_user
145
+ // Introduced: V0.1.0
146
+ // ============================================================================
147
+ workspaceMembers: (0, server_1.defineTable)({
148
+ workspaceId: values_1.v.id('workspaces'),
149
+ userId: values_1.v.id('users'),
150
+ role: values_1.v.union(values_1.v.literal('owner'), values_1.v.literal('admin'), values_1.v.literal('editor'), values_1.v.literal('viewer')),
151
+ permissions: values_1.v.optional(values_1.v.object({
152
+ canDeleteRecords: values_1.v.optional(values_1.v.boolean()),
153
+ canBulkImport: values_1.v.optional(values_1.v.boolean()),
154
+ canManageWorkflows: values_1.v.optional(values_1.v.boolean()),
155
+ canManageIntegrations: values_1.v.optional(values_1.v.boolean()),
156
+ canInviteMembers: values_1.v.optional(values_1.v.boolean()),
157
+ canViewAuditLog: values_1.v.optional(values_1.v.boolean()),
158
+ })),
159
+ addedAt: values_1.v.number(),
160
+ addedBy: values_1.v.optional(values_1.v.id('users')),
161
+ })
162
+ .index('by_workspace', ['workspaceId'])
163
+ .index('by_user', ['userId'])
164
+ .index('by_workspace_user', ['workspaceId', 'userId']),
165
+ // ============================================================================
166
+ // KEEP — contacts [EXTENDED V0.1.0: customFields, vpProfileId (OQ-3), isArchived/archivedAt (OQ-4)]
167
+ // Role V0.1.0: CRM core entity — people you sell to and work with.
168
+ // OQ-3: vpProfileId future-proofs VantagePeers cross-link for V0.2.
169
+ // OQ-4: soft delete via isArchived/archivedAt — hard delete admin-only after 30j grace.
170
+ // Multi-tenant key: workspaceId (mandatory).
171
+ // Indexes: by_workspace, by_workspace_created, by_workspace_type, by_owner,
172
+ // by_company, by_workspace_archived + search_contacts
173
+ // Introduced: V0.1.0
174
+ // ============================================================================
175
+ contacts: (0, server_1.defineTable)({
176
+ workspaceId: values_1.v.id('workspaces'),
177
+ firstName: values_1.v.string(),
178
+ lastName: values_1.v.string(),
179
+ email: values_1.v.optional(values_1.v.string()),
180
+ phone: values_1.v.optional(values_1.v.string()),
181
+ companyId: values_1.v.optional(values_1.v.id('companies')),
182
+ type: values_1.v.union(values_1.v.literal('lead'), values_1.v.literal('prospect'), values_1.v.literal('client'), values_1.v.literal('partner'), values_1.v.literal('other')),
183
+ source: values_1.v.optional(values_1.v.string()),
184
+ tags: values_1.v.optional(values_1.v.array(values_1.v.string())),
185
+ notes: values_1.v.optional(values_1.v.string()),
186
+ jobTitle: values_1.v.optional(values_1.v.string()),
187
+ linkedinUrl: values_1.v.optional(values_1.v.string()),
188
+ address: values_1.v.optional(values_1.v.string()),
189
+ leadScore: values_1.v.optional(values_1.v.number()),
190
+ lastScoredAt: values_1.v.optional(values_1.v.number()),
191
+ ownerId: values_1.v.string(),
192
+ lastContactedAt: values_1.v.optional(values_1.v.number()),
193
+ searchableText: values_1.v.string(),
194
+ // Extensions V0.1.0
195
+ customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())), // D-004 JSON column
196
+ vpProfileId: values_1.v.optional(values_1.v.string()), // OQ-3: VantagePeers cross-link V0.2
197
+ isArchived: values_1.v.optional(values_1.v.boolean()), // OQ-4: soft delete
198
+ archivedAt: values_1.v.optional(values_1.v.number()), // OQ-4: 30j grace period start
199
+ createdAt: values_1.v.number(),
200
+ updatedAt: values_1.v.number(),
201
+ })
202
+ .index('by_workspace', ['workspaceId'])
203
+ .index('by_workspace_created', ['workspaceId', 'createdAt'])
204
+ .index('by_workspace_type', ['workspaceId', 'type'])
205
+ .index('by_owner', ['ownerId'])
206
+ .index('by_company', ['companyId'])
207
+ .index('by_workspace_archived', ['workspaceId', 'isArchived'])
208
+ .searchIndex('search_contacts', {
209
+ searchField: 'searchableText',
210
+ filterFields: ['workspaceId', 'isArchived'],
211
+ }),
212
+ // ============================================================================
213
+ // KEEP — companies [EXTENDED V0.1.0: customFields, isArchived/archivedAt (OQ-4)]
214
+ // Role V0.1.0: CRM accounts/companies — pivot between contacts and deals.
215
+ // OQ-4: soft delete via isArchived/archivedAt — hard delete admin-only after 30j grace.
216
+ // Multi-tenant key: workspaceId (mandatory).
217
+ // Indexes: by_workspace, by_workspace_created, by_owner, by_domain,
218
+ // by_workspace_archived + search_companies
219
+ // Introduced: V0.1.0
220
+ // ============================================================================
221
+ companies: (0, server_1.defineTable)({
222
+ workspaceId: values_1.v.id('workspaces'),
223
+ name: values_1.v.string(),
224
+ domain: values_1.v.optional(values_1.v.string()),
225
+ industry: values_1.v.optional(values_1.v.string()),
226
+ size: values_1.v.optional(values_1.v.union(values_1.v.literal('1-10'), values_1.v.literal('11-50'), values_1.v.literal('51-200'), values_1.v.literal('201-1000'), values_1.v.literal('1000+'))),
227
+ website: values_1.v.optional(values_1.v.string()),
228
+ phone: values_1.v.optional(values_1.v.string()),
229
+ address: values_1.v.optional(values_1.v.string()),
230
+ description: values_1.v.optional(values_1.v.string()),
231
+ tags: values_1.v.optional(values_1.v.array(values_1.v.string())),
232
+ notes: values_1.v.optional(values_1.v.string()),
233
+ ownerId: values_1.v.string(),
234
+ contactCount: values_1.v.optional(values_1.v.number()),
235
+ dealCount: values_1.v.optional(values_1.v.number()),
236
+ totalDealValue: values_1.v.optional(values_1.v.number()),
237
+ searchableText: values_1.v.string(),
238
+ // Extensions V0.1.0
239
+ customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())), // D-004 JSON column
240
+ isArchived: values_1.v.optional(values_1.v.boolean()), // OQ-4: soft delete
241
+ archivedAt: values_1.v.optional(values_1.v.number()), // OQ-4: 30j grace period start
242
+ createdAt: values_1.v.number(),
243
+ updatedAt: values_1.v.number(),
244
+ })
245
+ .index('by_workspace', ['workspaceId'])
246
+ .index('by_workspace_created', ['workspaceId', 'createdAt'])
247
+ .index('by_owner', ['ownerId'])
248
+ .index('by_domain', ['domain'])
249
+ .index('by_workspace_archived', ['workspaceId', 'isArchived'])
250
+ .searchIndex('search_companies', {
251
+ searchField: 'searchableText',
252
+ filterFields: ['workspaceId', 'isArchived'],
253
+ }),
254
+ // ============================================================================
255
+ // KEEP — deals [EXTENDED V0.1.0: customFields, probability (OQ-2), isArchived/archivedAt (OQ-4)]
256
+ // Role V0.1.0: Pipeline opportunities with configurable stages + probability.
257
+ // OQ-2: probability (0-100) auto-set from pipelineStages.probability at moveStage or manual.
258
+ // OQ-4: soft delete via isArchived/archivedAt — hard delete admin-only after 30j grace.
259
+ // Multi-tenant key: workspaceId (mandatory).
260
+ // Indexes: by_workspace, by_workspace_stage, by_workspace_created, by_contact,
261
+ // by_company, by_owner, by_workspace_archived + search_deals
262
+ // Introduced: V0.1.0
263
+ // ============================================================================
264
+ deals: (0, server_1.defineTable)({
265
+ workspaceId: values_1.v.id('workspaces'),
266
+ title: values_1.v.string(),
267
+ stage: values_1.v.string(), // Configurable per workspace via pipelineStages
268
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
269
+ companyId: values_1.v.optional(values_1.v.id('companies')),
270
+ value: values_1.v.optional(values_1.v.number()),
271
+ probability: values_1.v.optional(values_1.v.number()), // 0-100, OQ-2 auto-set or manual
272
+ currency: values_1.v.optional(values_1.v.string()),
273
+ expectedCloseDate: values_1.v.optional(values_1.v.number()),
274
+ actualCloseDate: values_1.v.optional(values_1.v.number()),
275
+ description: values_1.v.optional(values_1.v.string()),
276
+ tags: values_1.v.optional(values_1.v.array(values_1.v.string())),
277
+ notes: values_1.v.optional(values_1.v.string()),
278
+ source: values_1.v.optional(values_1.v.string()),
279
+ lostReason: values_1.v.optional(values_1.v.string()),
280
+ ownerId: values_1.v.string(),
281
+ stageChangedAt: values_1.v.optional(values_1.v.number()),
282
+ lastActivityAt: values_1.v.optional(values_1.v.number()),
283
+ searchableText: values_1.v.string(),
284
+ // Extensions V0.1.0
285
+ customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())), // D-004 JSON column
286
+ isArchived: values_1.v.optional(values_1.v.boolean()), // OQ-4: soft delete
287
+ archivedAt: values_1.v.optional(values_1.v.number()), // OQ-4: 30j grace period start
288
+ createdAt: values_1.v.number(),
289
+ updatedAt: values_1.v.number(),
290
+ })
291
+ .index('by_workspace', ['workspaceId'])
292
+ .index('by_workspace_stage', ['workspaceId', 'stage'])
293
+ .index('by_workspace_created', ['workspaceId', 'createdAt'])
294
+ .index('by_contact', ['contactId'])
295
+ .index('by_company', ['companyId'])
296
+ .index('by_owner', ['ownerId'])
297
+ .index('by_workspace_archived', ['workspaceId', 'isArchived'])
298
+ .searchIndex('search_deals', {
299
+ searchField: 'searchableText',
300
+ filterFields: ['workspaceId', 'stage', 'isArchived'],
301
+ }),
302
+ // ============================================================================
303
+ // KEEP — activities [EXTENDED V0.1.0: customFields, actorType/actorId (OQ-1). NO remove mutation — OQ-4]
304
+ // Role V0.1.0: Immutable CRM audit trail — calls, emails, meetings, notes, tasks.
305
+ // OQ-1: actorType/actorId tracks Composio agent actions (actorId='agent:composio').
306
+ // OQ-4: activities are NEVER deleted — they ARE the audit trail. No isArchived by design.
307
+ // Note: No remove mutation exists or will be added. Salesforce doctrine.
308
+ // Multi-tenant key: workspaceId (mandatory).
309
+ // Indexes: by_workspace, by_workspace_type, by_workspace_created, by_contact,
310
+ // by_deal, by_company, by_owner
311
+ // Introduced: V0.1.0
312
+ // ============================================================================
313
+ activities: (0, server_1.defineTable)({
314
+ workspaceId: values_1.v.id('workspaces'),
315
+ type: values_1.v.union(values_1.v.literal('call'), values_1.v.literal('email'), values_1.v.literal('meeting'), values_1.v.literal('note'), values_1.v.literal('task'), values_1.v.literal('stage-change'), values_1.v.literal('email-received'), values_1.v.literal('email-sent'), values_1.v.literal('calendar-event'), values_1.v.literal('workflow-action')),
316
+ subject: values_1.v.string(),
317
+ description: values_1.v.optional(values_1.v.string()),
318
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
319
+ companyId: values_1.v.optional(values_1.v.id('companies')),
320
+ dealId: values_1.v.optional(values_1.v.id('deals')),
321
+ ownerId: values_1.v.string(),
322
+ occurredAt: values_1.v.number(),
323
+ dueAt: values_1.v.optional(values_1.v.number()),
324
+ completedAt: values_1.v.optional(values_1.v.number()),
325
+ // OQ-1: Composio actor tracking
326
+ actorType: values_1.v.optional(values_1.v.union(values_1.v.literal('user'), values_1.v.literal('agent'), values_1.v.literal('system'))),
327
+ actorId: values_1.v.optional(values_1.v.string()), // clerkId | 'agent:composio' | 'system'
328
+ // Extension V0.1.0
329
+ customFields: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())), // D-004 JSON column
330
+ createdAt: values_1.v.number(),
331
+ updatedAt: values_1.v.number(),
332
+ })
333
+ .index('by_workspace', ['workspaceId'])
334
+ .index('by_workspace_type', ['workspaceId', 'type'])
335
+ .index('by_workspace_created', ['workspaceId', 'createdAt'])
336
+ .index('by_contact', ['contactId'])
337
+ .index('by_deal', ['dealId'])
338
+ .index('by_company', ['companyId'])
339
+ .index('by_owner', ['ownerId']),
340
+ // ============================================================================
341
+ // KEEP — emailSync (keep as-is per OQ-1)
342
+ // Role V0.1.0: Composio Gmail sync. upsertFromSync wraps withAuditLog actorId='agent:composio'.
343
+ // Multi-tenant key: workspaceId (mandatory).
344
+ // Indexes: by_workspace, by_gmail_id, by_contact, by_workspace_sent
345
+ // Introduced: V0.1.0
346
+ // ============================================================================
347
+ emailSync: (0, server_1.defineTable)({
348
+ workspaceId: values_1.v.id('workspaces'),
349
+ gmailId: values_1.v.string(),
350
+ threadId: values_1.v.optional(values_1.v.string()),
351
+ subject: values_1.v.string(),
352
+ from: values_1.v.string(),
353
+ to: values_1.v.optional(values_1.v.array(values_1.v.string())),
354
+ snippet: values_1.v.optional(values_1.v.string()),
355
+ bodyPlain: values_1.v.optional(values_1.v.string()), // truncated 10KB
356
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
357
+ companyId: values_1.v.optional(values_1.v.id('companies')),
358
+ sentAt: values_1.v.number(),
359
+ syncedAt: values_1.v.number(),
360
+ })
361
+ .index('by_workspace', ['workspaceId'])
362
+ .index('by_gmail_id', ['gmailId'])
363
+ .index('by_contact', ['contactId'])
364
+ .index('by_workspace_sent', ['workspaceId', 'sentAt']),
365
+ // ============================================================================
366
+ // KEEP — calendarEvents (keep as-is per OQ-1)
367
+ // Role V0.1.0: Composio Google Calendar sync. Keep as-is per OQ-1.
368
+ // Multi-tenant key: workspaceId (mandatory).
369
+ // Indexes: by_workspace, by_google_event, by_contact, by_workspace_start
370
+ // Introduced: V0.1.0
371
+ // ============================================================================
372
+ calendarEvents: (0, server_1.defineTable)({
373
+ workspaceId: values_1.v.id('workspaces'),
374
+ googleEventId: values_1.v.string(),
375
+ title: values_1.v.string(),
376
+ description: values_1.v.optional(values_1.v.string()),
377
+ startTime: values_1.v.number(),
378
+ endTime: values_1.v.number(),
379
+ attendees: values_1.v.optional(values_1.v.array(values_1.v.object({
380
+ email: values_1.v.string(),
381
+ displayName: values_1.v.optional(values_1.v.string()),
382
+ }))),
383
+ contactId: values_1.v.optional(values_1.v.id('contacts')),
384
+ companyId: values_1.v.optional(values_1.v.id('companies')),
385
+ dealId: values_1.v.optional(values_1.v.id('deals')),
386
+ syncedAt: values_1.v.number(),
387
+ })
388
+ .index('by_workspace', ['workspaceId'])
389
+ .index('by_google_event', ['googleEventId'])
390
+ .index('by_contact', ['contactId'])
391
+ .index('by_workspace_start', ['workspaceId', 'startTime']),
392
+ // ============================================================================
393
+ // KEEP — integrations (keep as-is per OQ-1)
394
+ // Role V0.1.0: Composio connection tracking per workspace.
395
+ // Multi-tenant key: workspaceId (mandatory).
396
+ // Indexes: by_workspace, by_workspace_toolkit, by_workspace_status
397
+ // Introduced: V0.1.0
398
+ // ============================================================================
399
+ integrations: (0, server_1.defineTable)({
400
+ workspaceId: values_1.v.id('workspaces'),
401
+ toolkitSlug: values_1.v.string(),
402
+ connectionId: values_1.v.string(),
403
+ status: values_1.v.union(values_1.v.literal('active'), values_1.v.literal('disconnected')),
404
+ connectedAt: values_1.v.number(),
405
+ connectedBy: values_1.v.id('users'),
406
+ })
407
+ .index('by_workspace', ['workspaceId'])
408
+ .index('by_workspace_toolkit', ['workspaceId', 'toolkitSlug'])
409
+ .index('by_workspace_status', ['workspaceId', 'status']),
410
+ // ============================================================================
411
+ // NEW — custom_field_definitions (D-004)
412
+ // Role V0.1.0: Runtime field definitions per workspace per entityType.
413
+ // Values stored in customFields JSON column on each CRM record (not a side-table).
414
+ // 10 field types: string/number/boolean/date/select/multi-select/url/email/phone/json.
415
+ // D-003 field-level RBAC: visibility controls per-field access by workspace role.
416
+ // Multi-tenant key: workspaceId (mandatory).
417
+ // Indexes: by_workspace, by_workspace_entity, by_workspace_entity_name
418
+ // Introduced: V0.1.0
419
+ // ============================================================================
420
+ custom_field_definitions: (0, server_1.defineTable)({
421
+ workspaceId: values_1.v.id('workspaces'),
422
+ entityType: values_1.v.string(), // 'contact'|'deal'|'company'|'activity'|<custom_object_name>
423
+ name: values_1.v.string(), // slug e.g. 'industry_tier', 'churn_risk'
424
+ label: values_1.v.string(), // display e.g. 'Industry Tier', 'Churn Risk'
425
+ fieldType: values_1.v.union(values_1.v.literal('string'), values_1.v.literal('number'), values_1.v.literal('boolean'), values_1.v.literal('date'), values_1.v.literal('select'), values_1.v.literal('multi-select'), values_1.v.literal('url'), values_1.v.literal('email'), values_1.v.literal('phone'), values_1.v.literal('json')),
426
+ options: values_1.v.optional(values_1.v.array(values_1.v.object({
427
+ value: values_1.v.string(),
428
+ label: values_1.v.string(),
429
+ }))), // For select/multi-select
430
+ required: values_1.v.boolean(),
431
+ defaultValue: values_1.v.optional(values_1.v.union(values_1.v.string(), values_1.v.number(), values_1.v.boolean(), values_1.v.null())),
432
+ validation: values_1.v.optional(values_1.v.object({
433
+ pattern: values_1.v.optional(values_1.v.string()), // regex
434
+ min: values_1.v.optional(values_1.v.number()),
435
+ max: values_1.v.optional(values_1.v.number()),
436
+ })),
437
+ // D-003 field-level RBAC
438
+ visibility: values_1.v.union(values_1.v.literal('all'), values_1.v.literal('admin-only'), values_1.v.literal('role-restricted')),
439
+ visibleToRoles: values_1.v.optional(values_1.v.array(values_1.v.string())), // ['owner', 'admin', 'editor']
440
+ orderPosition: values_1.v.optional(values_1.v.number()),
441
+ createdBy: values_1.v.string(), // clerkId
442
+ createdAt: values_1.v.number(),
443
+ updatedAt: values_1.v.number(),
444
+ })
445
+ .index('by_workspace', ['workspaceId'])
446
+ .index('by_workspace_entity', ['workspaceId', 'entityType'])
447
+ .index('by_workspace_entity_name', ['workspaceId', 'entityType', 'name']),
448
+ // ============================================================================
449
+ // NEW — custom_object_definitions (D-005)
450
+ // Role V0.1.0: DDL runtime for custom objects — does not mutate Convex schema.
451
+ // Pattern: Salesforce/Airtable polymorphic — definitions metadata + single records table.
452
+ // Relationships field defines FK pointers in custom_object_records.data JSON.
453
+ // Multi-tenant key: workspaceId (mandatory).
454
+ // Indexes: by_workspace, by_workspace_name, by_workspace_archived
455
+ // Introduced: V0.1.0
456
+ // ============================================================================
457
+ custom_object_definitions: (0, server_1.defineTable)({
458
+ workspaceId: values_1.v.id('workspaces'),
459
+ name: values_1.v.string(), // slug e.g. 'property', 'vehicle', 'ticket'
460
+ label: values_1.v.string(), // 'Property'
461
+ pluralLabel: values_1.v.string(), // 'Properties'
462
+ icon: values_1.v.optional(values_1.v.string()),
463
+ description: values_1.v.optional(values_1.v.string()),
464
+ primaryFieldName: values_1.v.string(), // e.g. 'address' — shown as record title
465
+ searchableFields: values_1.v.array(values_1.v.string()), // fields included in searchableText rebuild
466
+ relationships: values_1.v.optional(values_1.v.array(values_1.v.object({
467
+ targetEntity: values_1.v.string(), // 'contact'|'deal'|'company'|<custom_object_name>
468
+ cardinality: values_1.v.union(values_1.v.literal('1-1'), values_1.v.literal('1-N'), values_1.v.literal('N-N')),
469
+ label: values_1.v.string(),
470
+ fieldName: values_1.v.string(), // e.g. 'contactId' — FK key in data JSON
471
+ }))),
472
+ createdBy: values_1.v.string(),
473
+ isArchived: values_1.v.optional(values_1.v.boolean()),
474
+ createdAt: values_1.v.number(),
475
+ updatedAt: values_1.v.number(),
476
+ })
477
+ .index('by_workspace', ['workspaceId'])
478
+ .index('by_workspace_name', ['workspaceId', 'name'])
479
+ .index('by_workspace_archived', ['workspaceId', 'isArchived']),
480
+ // ============================================================================
481
+ // NEW — custom_object_records (D-005)
482
+ // Role V0.1.0: Polymorphic single table for all custom objects.
483
+ // objectType discriminator + data JSON keyed by field names from definition.
484
+ // searchableText denormalized for full-text search across any custom object type.
485
+ // OQ-4: isArchived/archivedAt soft delete — NO hard delete without admin scope.
486
+ // Multi-tenant key: workspaceId (mandatory).
487
+ // Indexes: by_workspace_object, by_workspace_object_created, by_owner,
488
+ // by_workspace_archived + search_records
489
+ // Introduced: V0.1.0
490
+ // ============================================================================
491
+ custom_object_records: (0, server_1.defineTable)({
492
+ workspaceId: values_1.v.id('workspaces'),
493
+ objectType: values_1.v.string(), // matches custom_object_definitions.name
494
+ data: values_1.v.record(values_1.v.string(), values_1.v.any()), // JSON keyed by field name
495
+ ownerId: values_1.v.string(),
496
+ searchableText: values_1.v.string(), // rebuilt on each create/update
497
+ isArchived: values_1.v.optional(values_1.v.boolean()), // OQ-4: soft delete
498
+ archivedAt: values_1.v.optional(values_1.v.number()), // OQ-4: 30j grace period start
499
+ createdAt: values_1.v.number(),
500
+ updatedAt: values_1.v.number(),
501
+ })
502
+ .index('by_workspace_object', ['workspaceId', 'objectType'])
503
+ .index('by_workspace_object_created', ['workspaceId', 'objectType', 'createdAt'])
504
+ .index('by_owner', ['ownerId'])
505
+ .index('by_workspace_archived', ['workspaceId', 'isArchived'])
506
+ .searchIndex('search_records', {
507
+ searchField: 'searchableText',
508
+ filterFields: ['workspaceId', 'objectType', 'isArchived'],
509
+ }),
510
+ // ============================================================================
511
+ // NEW — audit_log (D-006)
512
+ // Role V0.1.0: Compliance trace of every CRM mutation. 7-year retention.
513
+ // withAuditLog() wrapper called on 100% of CRM mutations.
514
+ // OQ-1: actorType='agent' tracks Composio actions (actorId='agent:composio').
515
+ // OQ-4: soft delete → action='archive'; hard delete admin → action='delete'.
516
+ // fieldChanges captures before/after for diff views (GDPR right-to-erasure support).
517
+ // Multi-tenant key: workspaceId (mandatory).
518
+ // Indexes: by_workspace, by_workspace_timestamp, by_workspace_entity,
519
+ // by_workspace_actor, by_workspace_action
520
+ // Introduced: V0.1.0
521
+ // ============================================================================
522
+ audit_log: (0, server_1.defineTable)({
523
+ workspaceId: values_1.v.id('workspaces'),
524
+ actorId: values_1.v.string(), // clerkId | 'agent:composio' | 'system'
525
+ actorType: values_1.v.union(values_1.v.literal('user'), values_1.v.literal('agent'), values_1.v.literal('system')),
526
+ entityType: values_1.v.string(), // 'contact'|'deal'|'company'|'activity'|etc.
527
+ entityId: values_1.v.string(), // Convex Id as string
528
+ action: values_1.v.union(values_1.v.literal('create'), values_1.v.literal('update'), values_1.v.literal('delete'), values_1.v.literal('archive'), values_1.v.literal('restore'), values_1.v.literal('stage-change'), values_1.v.literal('owner-change'), values_1.v.literal('bulk-import'), values_1.v.literal('bulk-export'), values_1.v.literal('permission-change'), values_1.v.literal('workflow-triggered'), values_1.v.literal('workflow-action-executed')),
529
+ fieldChanges: values_1.v.optional(values_1.v.array(values_1.v.object({
530
+ fieldName: values_1.v.string(),
531
+ oldValue: values_1.v.union(values_1.v.string(), values_1.v.number(), values_1.v.boolean(), values_1.v.null()),
532
+ newValue: values_1.v.union(values_1.v.string(), values_1.v.number(), values_1.v.boolean(), values_1.v.null()),
533
+ }))),
534
+ metadata: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())),
535
+ ip: values_1.v.optional(values_1.v.string()),
536
+ userAgent: values_1.v.optional(values_1.v.string()),
537
+ timestamp: values_1.v.number(),
538
+ })
539
+ .index('by_workspace', ['workspaceId'])
540
+ .index('by_workspace_timestamp', ['workspaceId', 'timestamp'])
541
+ .index('by_workspace_entity', ['workspaceId', 'entityType', 'entityId'])
542
+ .index('by_workspace_actor', ['workspaceId', 'actorId'])
543
+ .index('by_workspace_action', ['workspaceId', 'action']),
544
+ // ============================================================================
545
+ // NEW — workflow_definitions (D-007)
546
+ // Role V0.1.0: Trigger-based workflow definitions. 5 trigger types × 5 action types.
547
+ // Engine: Convex mutation hooks + cron (time-based trigger type).
548
+ // Conditions evaluated at runtime. idempotency enforced in workflow_executions.
549
+ // Multi-tenant key: workspaceId (mandatory).
550
+ // Indexes: by_workspace, by_workspace_active, by_workspace_trigger
551
+ // Introduced: V0.1.0
552
+ // ============================================================================
553
+ workflow_definitions: (0, server_1.defineTable)({
554
+ workspaceId: values_1.v.id('workspaces'),
555
+ name: values_1.v.string(),
556
+ description: values_1.v.optional(values_1.v.string()),
557
+ isActive: values_1.v.boolean(),
558
+ trigger: values_1.v.object({
559
+ type: values_1.v.union(values_1.v.literal('record-created'), values_1.v.literal('record-updated'), values_1.v.literal('field-changed'), values_1.v.literal('stage-changed'), values_1.v.literal('time-based')),
560
+ entityType: values_1.v.optional(values_1.v.string()), // 'contact'|'deal'|'company'|'activity'
561
+ fieldName: values_1.v.optional(values_1.v.string()), // for field-changed
562
+ cron: values_1.v.optional(values_1.v.string()), // cron expr for time-based
563
+ conditions: values_1.v.optional(values_1.v.array(values_1.v.object({
564
+ field: values_1.v.string(),
565
+ operator: values_1.v.union(values_1.v.literal('eq'), values_1.v.literal('neq'), values_1.v.literal('gt'), values_1.v.literal('lt'), values_1.v.literal('in'), values_1.v.literal('contains')),
566
+ value: values_1.v.union(values_1.v.string(), values_1.v.number(), values_1.v.boolean(), values_1.v.array(values_1.v.string())),
567
+ }))),
568
+ }),
569
+ actions: values_1.v.array(values_1.v.object({
570
+ id: values_1.v.string(),
571
+ type: values_1.v.union(values_1.v.literal('update-record'), values_1.v.literal('create-record'), values_1.v.literal('send-email'), values_1.v.literal('send-message'), values_1.v.literal('log-activity')),
572
+ params: values_1.v.record(values_1.v.string(), values_1.v.any()),
573
+ onError: values_1.v.optional(values_1.v.union(values_1.v.literal('continue'), values_1.v.literal('halt'))),
574
+ })),
575
+ createdBy: values_1.v.string(),
576
+ createdAt: values_1.v.number(),
577
+ updatedAt: values_1.v.number(),
578
+ })
579
+ .index('by_workspace', ['workspaceId'])
580
+ .index('by_workspace_active', ['workspaceId', 'isActive'])
581
+ .index('by_workspace_trigger', ['workspaceId', 'trigger.type']),
582
+ // ============================================================================
583
+ // NEW — workflow_executions (D-007)
584
+ // Role V0.1.0: Execution trace per workflow run. idempotencyKey prevents double-run
585
+ // within the same minute. Replay possible via MCP tool replay_execution.
586
+ // onError='halt' → status='halted', alert workspace owner via send_message action.
587
+ // Multi-tenant key: workspaceId (mandatory).
588
+ // Indexes: by_workspace, by_workflow, by_workspace_status, by_idempotency
589
+ // Introduced: V0.1.0
590
+ // ============================================================================
591
+ workflow_executions: (0, server_1.defineTable)({
592
+ workspaceId: values_1.v.id('workspaces'),
593
+ workflowId: values_1.v.id('workflow_definitions'),
594
+ triggerEntityType: values_1.v.string(),
595
+ triggerEntityId: values_1.v.string(),
596
+ triggerType: values_1.v.string(),
597
+ idempotencyKey: values_1.v.string(), // hash(workflowId + entityId + floor(timestamp/60000))
598
+ status: values_1.v.union(values_1.v.literal('running'), values_1.v.literal('completed'), values_1.v.literal('failed'), values_1.v.literal('halted')),
599
+ actionLog: values_1.v.array(values_1.v.object({
600
+ actionId: values_1.v.string(),
601
+ status: values_1.v.union(values_1.v.literal('pending'), values_1.v.literal('completed'), values_1.v.literal('failed'), values_1.v.literal('skipped')),
602
+ result: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())),
603
+ error: values_1.v.optional(values_1.v.string()),
604
+ timestamp: values_1.v.number(),
605
+ })),
606
+ startedAt: values_1.v.number(),
607
+ completedAt: values_1.v.optional(values_1.v.number()),
608
+ })
609
+ .index('by_workspace', ['workspaceId'])
610
+ .index('by_workflow', ['workflowId'])
611
+ .index('by_workspace_status', ['workspaceId', 'status'])
612
+ .index('by_idempotency', ['idempotencyKey']),
613
+ // ============================================================================
614
+ // NEW — subscriptions (D-010)
615
+ // Role V0.1.0: Billing state per organization. Polar Cloud + Gumroad Pro Support.
616
+ // Webhook flow: Polar/Gumroad → convex/http → processBillingEvent → upsert here.
617
+ // Limits object enforced at query/mutation layer (record count checks, scope checks).
618
+ // Multi-tenant key: orgId (org-level billing, not workspace-level).
619
+ // Indexes: by_org, by_external_id, by_status, by_provider
620
+ // Introduced: V0.1.0
621
+ // ============================================================================
622
+ subscriptions: (0, server_1.defineTable)({
623
+ orgId: values_1.v.id('organizations'),
624
+ provider: values_1.v.union(values_1.v.literal('polar'), values_1.v.literal('gumroad')),
625
+ externalSubscriptionId: values_1.v.string(),
626
+ tier: values_1.v.union(values_1.v.literal('free'), values_1.v.literal('pro-support'), values_1.v.literal('cloud-starter'), values_1.v.literal('cloud-pro'), values_1.v.literal('cloud-enterprise')),
627
+ status: values_1.v.union(values_1.v.literal('trial'), values_1.v.literal('active'), values_1.v.literal('past-due'), values_1.v.literal('canceled'), values_1.v.literal('expired')),
628
+ scopes: values_1.v.array(values_1.v.string()), // ['vantage-crm:read', 'vantage-crm:write', ...]
629
+ limits: values_1.v.object({
630
+ maxWorkspaces: values_1.v.number(),
631
+ maxUsers: values_1.v.number(),
632
+ maxRecords: values_1.v.number(),
633
+ workflowsEnabled: values_1.v.boolean(),
634
+ }),
635
+ trialEndsAt: values_1.v.optional(values_1.v.number()),
636
+ startDate: values_1.v.number(),
637
+ renewalDate: values_1.v.optional(values_1.v.number()),
638
+ canceledAt: values_1.v.optional(values_1.v.number()),
639
+ metadata: values_1.v.optional(values_1.v.record(values_1.v.string(), values_1.v.any())),
640
+ createdAt: values_1.v.number(),
641
+ updatedAt: values_1.v.number(),
642
+ })
643
+ .index('by_org', ['orgId'])
644
+ .index('by_external_id', ['externalSubscriptionId'])
645
+ .index('by_status', ['status'])
646
+ .index('by_provider', ['provider']),
647
+ // ============================================================================
648
+ // NEW — oauth_clients (D-009)
649
+ // Role V0.1.0: OAuth2 clients for HTTP MCP mode (cloud customers).
650
+ // Auto-provisioned at Polar/Gumroad subscription. clientSecretHash = bcrypt.
651
+ // 4-tier scopes: vantage-crm:read / write / admin / workflow-trigger.
652
+ // Multi-tenant key: orgId (org-level OAuth clients).
653
+ // Indexes: by_org, by_client_id, by_subscription
654
+ // Introduced: V0.1.0
655
+ // ============================================================================
656
+ oauth_clients: (0, server_1.defineTable)({
657
+ orgId: values_1.v.id('organizations'),
658
+ subscriptionId: values_1.v.id('subscriptions'),
659
+ clientId: values_1.v.string(), // UUID generated at provision
660
+ clientSecretHash: values_1.v.string(), // bcrypt hash — never store raw secret
661
+ scopes: values_1.v.array(values_1.v.string()), // scopes granted per subscription tier
662
+ isActive: values_1.v.boolean(),
663
+ lastUsedAt: values_1.v.optional(values_1.v.number()),
664
+ revokedAt: values_1.v.optional(values_1.v.number()),
665
+ createdAt: values_1.v.number(),
666
+ })
667
+ .index('by_org', ['orgId'])
668
+ .index('by_client_id', ['clientId'])
669
+ .index('by_subscription', ['subscriptionId']),
670
+ // ============================================================================
671
+ // NEW — oauth_access_tokens (D-009)
672
+ // Role V0.1.0: Short-lived access tokens (1h TTL) issued by oauth_clients.
673
+ // tokenHash = sha256(rawToken) — NEVER store raw token in DB.
674
+ // revokedAt enables explicit revocation before expiry.
675
+ // Multi-tenant key: clientId (indirectly via oauth_clients → orgId).
676
+ // Indexes: by_token_hash, by_client, by_expires
677
+ // Introduced: V0.1.0
678
+ // ============================================================================
679
+ oauth_access_tokens: (0, server_1.defineTable)({
680
+ clientId: values_1.v.string(),
681
+ tokenHash: values_1.v.string(), // sha256(rawToken)
682
+ scopes: values_1.v.array(values_1.v.string()),
683
+ expiresAt: values_1.v.number(),
684
+ revokedAt: values_1.v.optional(values_1.v.number()),
685
+ createdAt: values_1.v.number(),
686
+ })
687
+ .index('by_token_hash', ['tokenHash'])
688
+ .index('by_client', ['clientId'])
689
+ .index('by_expires', ['expiresAt']),
690
+ });