fix(webhooks): source email_domain from is_primary, not org.domains[0]#4459
Conversation
WorkOS's `org.domains` array order is not stable. Orgs with a verified root + a `failed` www variant can have WorkOS list the www variant first, which overwrote `organizations.email_domain` on every `organization.updated` webhook fire even though our table's `organization_domains.is_primary=true` was already pointing at the correct root. Scope3 hit this in prod (escalation #334 follow-up): WorkOS: [{domain: 'www.scope3.com', state: 'failed'}, {domain: 'scope3.com', state: 'verified'}] Our DB: organization_domains.is_primary=true → scope3.com organizations.email_domain → www.scope3.com (drifted) `upsertWorkosDomain` already preserves `is_primary` across conflicts, so our table's choice survives. Reading `email_domain` from the same canonical source (`organization_domains.is_primary=true`) keeps the two in lockstep. Fallback to `org.domains[0]` covers the initial-sync case (cold start, no is_primary row yet); in normal operation the `upsertWorkosDomain` loop sets `i===0` as primary, so the SELECT finds a row. Downstream impact: lookups like `brand-enrichment.ts`'s `WHERE email_domain = $1` were missing Scope3's org row entirely. Adds: - `server/src/scripts/sync-email-domain-from-is-primary.ts` — backfill pre-fix drift. Applied on prod 2026-05-12 11:55 UTC for Scope3. - Integration test pinning the new behavior (non-primary first in WorkOS array stays at the canonical is_primary value). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Reviewed. Ready to ship. Correctness verified:
Backfill script: WHERE clause is correct. Test: Three cases pin the right behavior — non-primary-first preserves primary, cold sync uses Nit (non-blocking): workos-webhooks.ts:620-622 — the Nit (non-blocking): workos-sync-email-domain.test.ts cleanup deletes from Export of |
| */ | ||
|
|
||
| import { describe, it, expect, beforeAll, afterAll, beforeEach } from 'vitest'; | ||
| import { initializeDatabase, closeDatabase, getPool } from '../../src/db/client.js'; |
…_type (#4464) Every saved or serialized agent now carries an authorization_type discriminator plus matching selector. Copy/Download block when any agent lacks scope (catches registry-picker stubs and untouched v2 imports) or when the contact section has fields filled but no name. Refs Russell Stringham's #4459 thread question. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original script flipped any `organizations.email_domain` that didn't
match `organization_domains.is_primary=true`. Audit on prod revealed
three distinct classes inside that broad definition:
1. `null` — never had a webhook fire / missed migration 468.
Strictly safe to fill from canonical.
2. `www_drift` — Scope3 class. Webhook patched in #4459; opt-in via
`--include-www-drift` if another org regresses.
3. `mismatched` — subsidiary/M&A (LinkedIn↔Microsoft,
EdDomus↔quattroruote.it, Omnicom↔omc.com, Teal↔teal.works,
Cora AI↔noverift.ai). NOT a drift bug. These are
structural modeling questions for `brands.house_domain`
+ `brand_domain_aliases` and possibly separate org
records, not for an automated overwrite.
Default behavior now applies only class (1). Class (3) is reported and
never written. Class (2) requires explicit `--include-www-drift`.
Applied on prod 2026-05-12 12:02 UTC: 7 null cases backfilled (Diageo,
digital opua GmbH x2, harvingupta, Spectrum Reach, TVIQ, Willem LLC).
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes a Scope3-specific drift identified during the #4448 reconciliation: their
organizations.email_domainhad been set towww.scope3.comwhileorganization_domains.is_primary=truewas onscope3.com. Cause: the WorkOSorganization.updatedwebhook handler (syncOrganizationDomains) sourcedemail_domainfromorg.domains[0], and WorkOS's array order is not stable. Scope3 had[www.scope3.com (failed), scope3.com (verified)]in WorkOS, so every webhook fire overwroteemail_domainto the failed www variant.upsertWorkosDomainalready preservesis_primaryacross conflicts (so our table's choice survives webhook re-fires) — this change readsemail_domainfrom that same canonical source. Falls back toorg.domains[0]only for the initial-sync case (cold start, no is_primary row exists yet).Downstream impact of the bug
Lookups like
services/brand-enrichment.ts:772(SELECT … FROM organizations WHERE email_domain = $1) were missing Scope3's org row entirely — brand enrichment ran as thoughscope3.comhad no associated org.Included
server/src/routes/workos-webhooks.ts—syncOrganizationDomainsreadsemail_domainfromorganization_domains.is_primary=true. Export added so the integration test can call it directly (mirrors the existingupsertOrganizationDomainexport pattern).server/src/scripts/sync-email-domain-from-is-primary.ts— backfill script for any other org that drifted pre-fix. Dry-run by default;--applyto write.server/tests/integration/workos-sync-email-domain.test.ts— pins the new behavior. Three cases: (1) non-primary first in WorkOS array stays at the canonical primary, (2) initial cold-sync falls back toorg.domains[0], (3) personal orgs are not touched.Applied on prod
Scope3 backfill applied via fly ssh 2026-05-12 11:55 UTC:
The webhook fix in this PR prevents re-drift on the next WorkOS
organization.updatedfor Scope3.Test plan
email_domaindrift across non-personal orgs🤖 Generated with Claude Code