* fix(bank): never pre-check or mirror another company's accounts in the EB callback
At one-session banks (SEB) the PSU's single consent can cover accounts a
sibling company books. The OAuth callback stored whatever the session
returned into the active company: all pre-enabled, mirrored into its
cash_accounts, ledgers allocated from its chart: one 'Spara val' away
from booking another aktiebolag's transactions (user report F1,
2026-09-01).
The deliberate reuse path (findReusableSessions) already guards claimed
IBANs; the callback now runs the same check via
fetchCrossCompanyAccountContext:
- accounts claimed by another of the user's companies are stored
disabled + flagged (claimed_by_company_*), skipped by the
cash_accounts mirror, and the picker names the claiming company
- a 'Synkas ej' deselection made on any other connection row is carried
onto fresh rows (the recurring came-back-pre-checked complaint, C2)
- lookup failure fails closed: new accounts stored deselected
- accounts the row itself already carried keep their own state, so a
renewal can never switch a working feed off
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197wmwP6zNaYvsGfbZuQHGA
* fix(bank): close the skeptic-found holes in the cross-company claim guard
Consolidated fixes from the three-skeptic review of PR #2116 (all three
refuted the first cut):
- Active-company standing state (enabled cash_accounts + enabled
accounts on its live-ish connection rows) now outranks sibling claims
company-wide, not row-wide: a bank-list renewal arrives on a FRESH row
with no priors, and the old row-local check would have let a sibling
claim switch a working feed off while supersede demoted its cash row.
- pending_selection rows no longer claim accounts or feed deselection
memory: their flags are unconfirmed callback output (including this
guard's own fail-closed writes), so an abandoned picker or a transient
lookup error can no longer poison later connects.
- Guard-disabled accounts are never mirrored from the callback:
upsertFromPsd2 with enabled:false for a new-to-row account could
promote the seeded primary 1930 manual row and flip it to disabled
under a foreign identity.
- The selection save skips ledger allocation and the cash_accounts
mirror for disabled never-mirrored accounts, so 'no cash row, no 19xx
slot burned' holds past the mandatory Spara val, and strips the
claimed_by_*/deselected flags when the user deliberately enables an
account.
- Deselection carry is no longer silent: deselected_elsewhere flag +
picker note 'Tidigare bortvald'.
- Claim lookups paginate via fetchAllRows: the bare select's silent
1000-row PostgREST cap failed open for exactly the multi-company
consultants the guard exists for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197wmwP6zNaYvsGfbZuQHGA
* fix(bank): claim-guard round 2: pending_selection claims asymmetrically, paged reads ordered
Skeptic re-verification of 25a339810 found two holes:
- Excluding pending_selection rows from claims reopened the
attach-to-picker window: an attach-created row holds deliberately
offered enabled accounts with no cash_accounts rows until its picker
is saved, and a full-OAuth connect in another company inside that
window could take the same physical account. Enabled accounts on
pending_selection rows claim again; their disabled flags still stay
out of the deselection memory (unconfirmed callback output, including
the guard's own fail-closed writes).
- Both fetchAllRows claim queries now order('id'): unordered .range()
pagination can silently skip rows at page boundaries, and a skipped
row is a missed claim, failing open at exactly the 1000+-row scale
the pagination was added for.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0197wmwP6zNaYvsGfbZuQHGA
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>