koala73/worldmonitor · error · ConvexError
ACCOUNT_OWNER_BINDING_MISMATCH
ACCOUNT_OWNER_BINDING_MISMATCH
Error message
ACCOUNT_OWNER_BINDING_MISMATCH
What it means
Raised by syncCompanyMonitoringAccountFromEntitlement when an existing account row's owner binding does not match the entitlement's owner: the account was found (e.g. by keyed fence or logical id) but its ownerUserId disagrees with the user being synced. This is a data-integrity guard — an account must never be mutated on behalf of a different owner, including replayed or delayed activations, and terminal rows can never be re-bound.
Solutions
- Compare the account row's ownerUserId with the entitlement's owner to identify the corruption source
- If the row is stale or orphaned, terminalize it and provision a fresh account rather than rebinding
- Audit recent writes for a bug that passed the wrong ownerUserId into the sync
Defensive patterns
Strategy: validation
When it happens
Trigger: Thrown at convex/companyMonitoring/accounts.ts:195 when the library encounters an invalid state.
Common situations: See trigger scenarios.
AI-assisted analysis of koala73/worldmonitor@eeab0a219f (2026-08-21).
Data as JSON: /api/errors/1678b117c1c20499.
Report an issue: GitHub.
Appendix: source
Thrown at convex/companyMonitoring/accounts.ts:195
companyCount: 0,
companyLimit: COMPANY_LIMIT,
snapshotGeneration: 0,
purgeGeneration: 0,
purgePhase: "none",
destructivePurgeStarted: false,
pendingReactivation: false,
claimPolicyVersion: COMPANY_MONITORING_CLAIM_POLICY_VERSION,
createdAt: now,
updatedAt: now,
});
await scheduleScopedKeyCacheInvalidation(ctx, userId);
return ctx.db.get(id);
}
// Terminal rows intentionally retain only the keyed fence and logical id.
// A replayed or delayed activation can find the row, but can never mutate it.
if (existing.terminalReason || existing.lifecycle === "denied") return existing;
if (existing.ownerUserId !== userId) throw new ConvexError("ACCOUNT_OWNER_BINDING_MISMATCH");
if (existing.ownerFenceHash !== ownerFenceHash) {
await ctx.db.patch(existing._id, { ownerFenceHash });
if (existing.purgePhase !== "none" && existing.purgePhase !== "complete") {
// Jobs scheduled before rotation still carry the old hash and will become
// stale after migration. Seed the same generation under the current key.
const delayMs = existing.purgePhase === "pending" && existing.purgeAfter
? existing.purgeAfter - Date.now()
: 0;
await scheduleAccountPurge(ctx, ownerFenceHash, existing.purgeGeneration, delayMs);
}
existing = { ...existing, ownerFenceHash };
}
const semanticChanged = existing.entitlementDigest !== canonical.digest;
const now = Date.now();
// A completed generation proves every company payload and claim was scrubbed.
// Reuse the same nonterminal owner root as an empty portfolio; terminal rootsView on GitHub (pinned to eeab0a219f)