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

  1. Compare the account row's ownerUserId with the entitlement's owner to identify the corruption source
  2. If the row is stale or orphaned, terminalize it and provision a fresh account rather than rebinding
  3. 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 roots

View on GitHub (pinned to eeab0a219f)