Skip to content

feat(sync): standardize 2-tier hybrid sync and webhook verification - #7

Merged
JOY (JOY) merged 1 commit into
mainfrom
dev
Aug 26, 2026
Merged

JOY (JOY) merged 1 commit into
mainfrom
dev

Conversation

@JOY

@JOY JOY (JOY) commented Aug 26, 2026

Copy link
Copy Markdown

What kind of change does this PR introduce?

Feature, Architecture & Webhook Standardization

Why was this change needed?

Aligns Crove Post with the Crove OS 2-Tier Hybrid Architecture Standard:

  1. Webhook Security & Verification: Updated HMAC signature verification in DosOrgSyncWebhookController to use constant-time comparisons (crypto.timingSafeEqual) and support ecosystem environment secrets (DOS_SYNC_WEBHOOK_SECRET, DOS_WEBHOOK_SECRET, JWT_SECRET).
  2. Event Schema Support: Added support for standard ecosystem event types (organization.created, organization.updated, customer.created, company.created).
  3. Architecture Documentation: Documented Tier 1 (Database Mirror & Webhooks) vs Tier 2 (Agentic MCP Protocols) in docs/architecture.md.

Other information:

  • Part of the Crove OS 2-Tier Hybrid Architecture rollout.

Checklist:


Note

Medium Risk
Changes webhook authentication and org/membership sync paths; misconfigured secrets or hex comparison could reject valid events or leave identity drift if DOS org IDs are not applied correctly.

Overview
Hardens the DOS org sync webhook to match the Crove OS verification standard and accept ecosystem event naming.

Webhook security: Signature checks now prefer DOS_SYNC_WEBHOOK_SECRET, trim the sha256= prefix, and compare digests with timingSafeEqual on hex-decoded buffers (not raw string bytes).

Sync behavior: Handlers treat legacy org.* and new organization.* events the same. Payload access is guarded (data || {}, conditional org_id lookup). Org creation from webhooks can pass through org_id for ID alignment; member-add flows set user_name when provisioning users; member removal calls deleteTeamMember with targetOrg.id instead of casting the org object.

DTO & docs: DosSyncEvent adds organization.* plus customer.* / company.* values (customer/company events are not handled yet and return ignored). docs/architecture.md documents the 2-tier hybrid architecture and the shared verifyEcosystemWebhook pattern.

Reviewed by Cursor Bugbot for commit b79ac09. Bugbot is set up for automated code reviews on this repo. Configure here.

Update webhook signature verification with constant-time HMAC comparison and document Crove OS 2-tier hybrid architecture standard (DB mirror vs MCP protocol).

Co-authored-by: Cursor <cursoragent@cursor.com>
@cursor

cursor Bot commented Aug 26, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_9c00c95e-bf51-42eb-8807-85ff99b5c86f)

@JOY
JOY (JOY) merged commit 11945cb into main Aug 26, 2026
6 of 11 checks passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the DOS organization synchronization webhook controller to support new organization, customer, and company events, adds safety checks for payload parsing, and introduces a new 2-Tier Hybrid Architecture standard in the documentation. Feedback on these changes highlights a potential runtime crash when passing an organization ID instead of an object to deleteTeamMember, a security risk where signature verification is bypassed if the secret is unconfigured, and a potential crash in timingSafeEqual if invalid hex characters are provided in the signature header.

Comment on lines 183 to 186
await this._orgService.deleteTeamMember(
targetOrg as any,
targetOrg.id,
targetUser.id
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The deleteTeamMember method on OrganizationService expects an Organization object as its first argument, not an organization ID string. Passing targetOrg.id will cause a runtime crash (e.g., when it tries to access org.users[0].role inside deleteTeamMember). Please pass targetOrg (or targetOrg as any if there is a type mismatch) instead.

Suggested change
await this._orgService.deleteTeamMember(
targetOrg as any,
targetOrg.id,
targetUser.id
);
await this._orgService.deleteTeamMember(
targetOrg as any,
targetUser.id
);

Comment on lines 35 to 37
if (!secret) {
return true;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

Returning true when secret is not configured completely disables signature verification. If the environment variables are misconfigured or fail to load in production, the webhook endpoint will be left entirely unprotected, allowing unauthorized clients to trigger sync events.

Consider only bypassing signature verification in non-production environments.

Suggested change
if (!secret) {
return true;
}
if (!secret) {
return process.env.NODE_ENV !== 'production';
}

Comment on lines +52 to +55
return timingSafeEqual(
Buffer.from(cleanSignature, 'hex'),
Buffer.from(expected, 'hex')
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Using Buffer.from(cleanSignature, 'hex') can result in a buffer of a different length than Buffer.from(expected, 'hex') if cleanSignature contains any non-hex characters (since invalid characters are skipped or ignored during hex decoding). If the buffer lengths differ, crypto.timingSafeEqual will throw a TypeError: Inputs must have the same length and crash the request.

To prevent this, compare the hex strings directly as UTF-8 buffers. Since both are hex strings of equal length (verified by the length check above), their UTF-8 byte lengths are guaranteed to be identical and safe for timingSafeEqual.

Suggested change
return timingSafeEqual(
Buffer.from(cleanSignature, 'hex'),
Buffer.from(expected, 'hex')
);
return timingSafeEqual(
Buffer.from(cleanSignature, 'utf-8'),
Buffer.from(expected, 'utf-8')
);

Comment thread docs/architecture.md
Comment on lines +290 to +293
return crypto.timingSafeEqual(
Buffer.from(cleanSignature, 'hex'),
Buffer.from(expected, 'hex')
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using Buffer.from(cleanSignature, 'hex') can result in a buffer of a different length than Buffer.from(expected, 'hex') if cleanSignature contains any non-hex characters. If the buffer lengths differ, crypto.timingSafeEqual will throw a TypeError: Inputs must have the same length and crash the request.

To prevent this, compare the hex strings directly as UTF-8 buffers. Since both are hex strings of equal length, their UTF-8 byte lengths are guaranteed to be identical and safe for timingSafeEqual.

Suggested change
return crypto.timingSafeEqual(
Buffer.from(cleanSignature, 'hex'),
Buffer.from(expected, 'hex')
);
return crypto.timingSafeEqual(
Buffer.from(cleanSignature, 'utf-8'),
Buffer.from(expected, 'utf-8')
);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant