diff --git a/apps/backend/src/api/routes/dos-org-sync.controller.ts b/apps/backend/src/api/routes/dos-org-sync.controller.ts index dfe33b6f9c..7e08d3a294 100644 --- a/apps/backend/src/api/routes/dos-org-sync.controller.ts +++ b/apps/backend/src/api/routes/dos-org-sync.controller.ts @@ -28,7 +28,10 @@ export class DosOrgSyncWebhookController { ) {} private verifySignature(rawBody: string, signatureHeader?: string): boolean { - const secret = process.env.DOS_WEBHOOK_SECRET || process.env.JWT_SECRET; + const secret = + process.env.DOS_SYNC_WEBHOOK_SECRET || + process.env.DOS_WEBHOOK_SECRET || + process.env.JWT_SECRET; if (!secret) { return true; } @@ -37,16 +40,19 @@ export class DosOrgSyncWebhookController { return false; } + const cleanSignature = signatureHeader.replace(/^sha256=/, '').trim(); const expected = createHmac('sha256', secret) .update(rawBody) .digest('hex'); - const provided = signatureHeader.replace(/^sha256=/, ''); - if (provided.length !== expected.length) { + if (cleanSignature.length !== expected.length) { return false; } - return timingSafeEqual(Buffer.from(provided), Buffer.from(expected)); + return timingSafeEqual( + Buffer.from(cleanSignature, 'hex'), + Buffer.from(expected, 'hex') + ); } @Post('/dos-org-sync') @@ -62,9 +68,9 @@ export class DosOrgSyncWebhookController { } const { event, data } = payload; - const { org_id, org_name, user_id, user_email, user_name, role } = data; + const { org_id, org_name, user_id, user_email, user_name, role } = data || {}; - let targetOrg = await this._orgService.getOrgById(org_id); + let targetOrg = org_id ? await this._orgService.getOrgById(org_id) : null; if (!targetOrg && org_name) { targetOrg = await this._orgService.findOrgByName(org_name); } @@ -78,13 +84,15 @@ export class DosOrgSyncWebhookController { } switch (event) { - case DosSyncEvent.ORG_CREATED: { + case DosSyncEvent.ORG_CREATED: + case DosSyncEvent.ORGANIZATION_CREATED: { if (!targetOrg) { if (targetUser) { await this._orgService.createOrgForExistingUser( targetUser.id, org_name || 'Organization', - 'SUPERADMIN' + 'SUPERADMIN', + org_id ); } else if (user_email) { const created = await this._orgService.createOrgAndUser( @@ -110,21 +118,24 @@ export class DosOrgSyncWebhookController { return { success: true, event, status: 'processed' }; } - case DosSyncEvent.ORG_UPDATED: { + case DosSyncEvent.ORG_UPDATED: + case DosSyncEvent.ORGANIZATION_UPDATED: { if (targetOrg && org_name) { await this._orgService.updateOrganizationName(targetOrg.id, org_name); } return { success: true, event, status: 'processed' }; } - case DosSyncEvent.ORG_DELETED: { + case DosSyncEvent.ORG_DELETED: + case DosSyncEvent.ORGANIZATION_DELETED: { if (targetOrg) { await this._orgService.deleteOrganization(targetOrg.id); } return { success: true, event, status: 'processed' }; } - case DosSyncEvent.ORG_MEMBER_ADDED: { + case DosSyncEvent.ORG_MEMBER_ADDED: + case DosSyncEvent.ORGANIZATION_MEMBER_ADDED: { if (targetOrg && targetUser) { const appRole = role === 'MEMBER' ? 'USER' : 'ADMIN'; await this._orgService.addUserToOrg( @@ -148,6 +159,12 @@ export class DosOrgSyncWebhookController { '127.0.0.1', 'dos-webhook-sync' ); + if (user_name) { + await this._userService.changePersonal( + created.users[0].user.id, + { fullname: user_name, bio: '' } + ); + } const appRole = role === 'MEMBER' ? 'USER' : 'ADMIN'; await this._orgService.addUserToOrg( created.users[0].user.id, @@ -160,10 +177,11 @@ export class DosOrgSyncWebhookController { return { success: true, event, status: 'processed' }; } - case DosSyncEvent.ORG_MEMBER_REMOVED: { + case DosSyncEvent.ORG_MEMBER_REMOVED: + case DosSyncEvent.ORGANIZATION_MEMBER_REMOVED: { if (targetOrg && targetUser) { await this._orgService.deleteTeamMember( - targetOrg as any, + targetOrg.id, targetUser.id ); } diff --git a/docs/architecture.md b/docs/architecture.md index 8b26584ce2..bb91b6b340 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -242,3 +242,55 @@ X-DOS-Signature: sha256= | `organizations.id` | `Organization.id` | `workspace.id` | `Organisation.id` | `Team.id` | | `organizations.name` | `Organization.name` | `workspace.name` | `Organisation.name` | `Team.name` | | `org_members.role` | `UserOrganization.role` | `workspaceMember.role` | `OrganisationMember.role` | `Membership.role` | + +--- + +## 8. 🏗️ 2-Tier Hybrid Architecture Standard + +The Crove OS ecosystem adopts a strict **2-Tier Separation of Concerns** between high-performance local database mirrors and deep agentic tool calling: + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────┐ +│ CROVE OS 2-TIER HYBRID ARCHITECTURE │ +├──────────────────────────────────────────┬──────────────────────────────────────────────┤ +│ TIER 1: Identity & Entity Data Sync │ TIER 2: Deep Agentic Business Actions │ +│ (Companies, Customers, Organizations) │ (Create Deals, Renewals, Assign Tasks) │ +├──────────────────────────────────────────┼──────────────────────────────────────────────┤ +│ DATABASE SYNCHRONIZATION │ MCP PROTOCOL │ +│ (PostgreSQL Mirror / < 5ms Latency) │ (Model Context Protocol Tool Calling) │ +│ │ │ │ │ +│ • Master SSOT: DOS.Me & Twenty CRM │ • twenty_crm.create_opportunity(...) │ +│ • Local Mirrors: post.* / desk.* │ • twenty_crm.get_subscription_status(...) │ +│ • Bi-directional Webhook Dispatch │ • twenty_crm.create_task(...) │ +│ • JIT (Just-In-Time) Sync on Login │ • crove_sign.get_contracts(...) │ +│ • Event Ingress: api.dos.me/internal/ │ • Deep business validation & side-effects │ +│ events/publish │ │ +└──────────────────────────────────────────┴──────────────────────────────────────────────┘ +``` + +### 8.1. Webhook Signature Verification Standard + +All satellite applications verify inbound events using constant-time HMAC-SHA256 comparison: + +```typescript +import * as crypto from 'crypto'; + +export function verifyEcosystemWebhook( + rawBody: string | Buffer, + signatureHeader: string, + secret: string +): boolean { + if (!signatureHeader || !secret) return false; + const cleanSignature = signatureHeader.replace(/^sha256=/, '').trim(); + const expected = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('hex'); + if (cleanSignature.length !== expected.length) return false; + return crypto.timingSafeEqual( + Buffer.from(cleanSignature, 'hex'), + Buffer.from(expected, 'hex') + ); +} +``` + diff --git a/libraries/nestjs-libraries/src/dtos/webhooks/dos-org-sync.dto.ts b/libraries/nestjs-libraries/src/dtos/webhooks/dos-org-sync.dto.ts index 18c45b7d2f..399cc6f2c6 100644 --- a/libraries/nestjs-libraries/src/dtos/webhooks/dos-org-sync.dto.ts +++ b/libraries/nestjs-libraries/src/dtos/webhooks/dos-org-sync.dto.ts @@ -14,6 +14,15 @@ export enum DosSyncEvent { ORG_DELETED = 'org.deleted', ORG_MEMBER_ADDED = 'org.member_added', ORG_MEMBER_REMOVED = 'org.member_removed', + ORGANIZATION_CREATED = 'organization.created', + ORGANIZATION_UPDATED = 'organization.updated', + ORGANIZATION_DELETED = 'organization.deleted', + ORGANIZATION_MEMBER_ADDED = 'organization.member.added', + ORGANIZATION_MEMBER_REMOVED = 'organization.member.removed', + CUSTOMER_CREATED = 'customer.created', + CUSTOMER_UPDATED = 'customer.updated', + COMPANY_CREATED = 'company.created', + COMPANY_UPDATED = 'company.updated', } export class DosOrgSyncDataDto {