{"record":{"id":"1678b117c1c20499","repo":"koala73/worldmonitor","slug":"account-owner-binding-mismatch","errorCode":"ACCOUNT_OWNER_BINDING_MISMATCH","errorMessage":"ACCOUNT_OWNER_BINDING_MISMATCH","messagePattern":"ACCOUNT_OWNER_BINDING_MISMATCH","errorType":"error_code","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/accounts.ts","lineNumber":195,"sourceCode":"      companyCount: 0,\n      companyLimit: COMPANY_LIMIT,\n      snapshotGeneration: 0,\n      purgeGeneration: 0,\n      purgePhase: \"none\",\n      destructivePurgeStarted: false,\n      pendingReactivation: false,\n      claimPolicyVersion: COMPANY_MONITORING_CLAIM_POLICY_VERSION,\n      createdAt: now,\n      updatedAt: now,\n    });\n    await scheduleScopedKeyCacheInvalidation(ctx, userId);\n    return ctx.db.get(id);\n  }\n\n  // Terminal rows intentionally retain only the keyed fence and logical id.\n  // A replayed or delayed activation can find the row, but can never mutate it.\n  if (existing.terminalReason || existing.lifecycle === \"denied\") return existing;\n  if (existing.ownerUserId !== userId) throw new ConvexError(\"ACCOUNT_OWNER_BINDING_MISMATCH\");\n\n  if (existing.ownerFenceHash !== ownerFenceHash) {\n    await ctx.db.patch(existing._id, { ownerFenceHash });\n    if (existing.purgePhase !== \"none\" && existing.purgePhase !== \"complete\") {\n      // Jobs scheduled before rotation still carry the old hash and will become\n      // stale after migration. Seed the same generation under the current key.\n      const delayMs = existing.purgePhase === \"pending\" && existing.purgeAfter\n        ? existing.purgeAfter - Date.now()\n        : 0;\n      await scheduleAccountPurge(ctx, ownerFenceHash, existing.purgeGeneration, delayMs);\n    }\n    existing = { ...existing, ownerFenceHash };\n  }\n\n  const semanticChanged = existing.entitlementDigest !== canonical.digest;\n  const now = Date.now();\n  // A completed generation proves every company payload and claim was scrubbed.\n  // Reuse the same nonterminal owner root as an empty portfolio; terminal roots","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/companyMonitoring/accounts.ts#L177-L213","documentation":"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.","triggerScenarios":"Thrown at convex/companyMonitoring/accounts.ts:195 when the library encounters an invalid state.","commonSituations":"See trigger scenarios.","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"],"exampleFix":null,"handlingStrategy":"validation","validationCode":null,"typeGuard":null,"tryCatchPattern":null,"preventionTips":[],"tags":[],"backgroundTag":null,"analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}