Jakob WennbergandClaude Opus 4.8 305f469fc3 harden(db): tenant backstop — payment company-consistency triggers + write-RPC guards (P0-2) (#680)
* harden(security): payment-row company-consistency triggers (tenant backstop)

invoice_payments and supplier_invoice_payments are the only two child tables
carrying BOTH a parent FK and their own company_id. A row whose company_id
disagrees with its parent's company_id is a tenant-isolation defect that would
surface a foreign tenant's payment in this company's AR/AP ledger. RLS scopes
by company_id but never cross-checks the parent, so nothing at the DB layer
guaranteed the invariant.

- Pre-flight DO block: fail the migration loudly (listing offending ids) if any
  existing row already violates child.company_id = parent.company_id, rather
  than arm a trigger over dirty data that can never be updated again.
- enforce_payment_company_consistency(): one INVOKER trigger function
  parameterized on TG_TABLE_NAME, wired BEFORE INSERT OR UPDATE OF
  (company_id, parent_fk) on both payment tables; raises on mismatch. Matches
  the SECURITY posture of the sibling enforcement triggers in migration 017.
- pg-real coverage in tests/pg/payment-company-consistency.pg.test.ts: matching
  pair inserts ok; cross-tenant insert + cross-tenant UPDATE raise; both the
  customer and supplier side.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* harden(security): tenant guards on six SECURITY DEFINER write RPCs (backstop)

bulk_book_transactions, match_batch_allocate, mark_entry_as_opening_balance,
reserve_voucher_range, release_voucher_range and rotate_company_inbox are all
SECURITY DEFINER and EXECUTE-able by `authenticated`, so an authenticated user
could call them via PostgREST with ANOTHER company's p_company_id. Three already
carried an auth.uid()-based membership check and rotate_company_inbox an
owner/admin gate, but the two voucher-range RPCs had NO tenant check at all.

Adds the canonical claims-based guard (mirrors
20260615120000_link_voucher_rpcs_tenant_guard.sql lines 54-69) at the top of
each body: for anon/authenticated callers, membership of p_company_id
(public.user_company_ids()) is required else RAISE 42501; service_role and
no-claims callers (migrations, pg-harness, MCP / API-key paths whose company
scoping happens in TS) bypass BY DESIGN. Each function body is otherwise copied
verbatim from its latest definition; existing GRANTs re-applied.

pg-real coverage in tests/pg/securitydefiner_write_rpc_tenant_guards.pg.test.ts:
per RPC — userA session targeting companyB raises 42501; targeting own company
passes the guard (succeeds or yields a non-42501 domain outcome, documented
inline); a no-claims bare-pool cross-tenant call bypasses the new guard,
proving the service-role / MCP paths are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(db): renumber tenant-backstop migrations to 20260619130000/130100

PR1 (agent attribution) claimed the 20260619120000 version slot in the same
batch; Supabase migration versions must be unique across the repo, so the
tenant-backstop pair moves to 130000/130100. Filename-only change plus the
matching doc-comment references in the two pg tests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(db): restore source comments dropped in copied RPC bodies

The guarded redefinitions of bulk_book_transactions and match_batch_allocate
must be byte-verbatim copies of their latest sources (modulo the inserted
tenant-guard block) so the next CREATE OR REPLACE copy keeps full provenance.
Restores the Round-2/Round-3 compliance-fix annotations that were lost in the
copy. Verified mechanically: zero residual diff vs sources after stripping the
guard block, for all six functions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(db): drop raise-guards from bulk_book/match_batch — they break the jsonb error contract

Local full-migration replay + pg-real run surfaced that prepending the
42501 raise-guard to bulk_book_transactions and match_batch_allocate
changes their error contract for authenticated cross-tenant callers: both
already enforce membership in-function and return structured domain errors
(BULK_BOOK_UNAUTHORIZED / BATCH_UNAUTHORIZED) that routes, MCP tools, and
their existing pg tests branch on. The guard added no isolation (they were
tenant-safe) but broke that contract. The migration now guards only the
four RPCs where it is sound: mark_entry_as_opening_balance (P0001→42501,
still an exception), rotate_company_inbox (already 42501), and the two
genuinely unguarded voucher-range RPCs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(db): compliance-review round — log hygiene, explicit INVOKER, anon revoke, UPDATE-path test

Addresses the compliance-swarm findings on this PR:
- Pre-flight dirty-data check now raises with COUNTS only; the row ids move
  to RAISE NOTICE so error pipelines do not ingest identifier dumps
  (ASVS V8.2.1 / SOC 2 CC6.1).
- enforce_payment_company_consistency() declares SECURITY INVOKER explicitly
  — the default was already INVOKER; this makes the security model
  self-documenting.
- REVOKE ... FROM PUBLIC, anon on reserve/release_voucher_range and
  rotate_company_inbox, matching the mark_entry_as_opening_balance pattern.
- Adds the missing supplier_invoice_payments UPDATE-path trigger probe
  (SOC 2 PI1.3).

Dismissed as by-design: the JWT-claim trust boundary (set_config requires
direct SQL access, which already bypasses by design — same model as
20260615120000).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(db): voucher-range compliance guards + FK-rerouting trigger probes

Review round 2 on this PR:

Swedish compliance review (both pre-existing function behaviour, hardened
while the PR owns these bodies):
- reserve/release_voucher_range now refuse closed/locked fiscal periods
  (BFL 5 kap 5§ — the sequence of a locked period is räkenskapsinformation;
  mirrors mark_entry_as_opening_balance).
- release_voucher_range asserts no verifikat exist in the released range
  before rolling last_number back (BFL 5 kap 6-7§ — never re-issue or orphan
  posted verifikationsnummer). Neither guard can fire in the legit SIE-import
  flow, which only releases numbers above its highest inserted verifikat into
  an open period — and the import caller treats a failed release as non-fatal.

Greptile P2: the UPDATE OF <parent_fk> trigger leg was never probed — added
cross-tenant FK-rerouting rejection tests for both payment tables (the
supplier company_id UPDATE probe landed in the previous commit).

Verified: full migration replay on fresh supabase/postgres + 333/333 pg-real
green on an origin/main merge (incl. merged #678).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(test): release-succeeds probe must persist — callBare rolls back

The legit-path release test asserted last_number after calling the RPC via
callBare, whose BEGIN...ROLLBACK wrapper undoes the UPDATE before the
assertion reads it (caught in CI; the local pre-push replay had validated the
branch's committed state, not the then-uncommitted test). Call the RPC
directly on the pool, like the engine pg tests do for persisting calls.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:42:41 +02:00
2026-06-05 17:26:40 +02:00
2026-06-05 17:26:40 +02:00
2026-06-05 17:26:40 +02:00
2026-06-02 13:25:48 +02:00
2026-05-06 11:12:02 +02:00
2026-04-22 15:33:04 +02:00
2026-05-28 15:43:48 +02:00
2026-05-22 12:22:15 +02:00

Accounted

Open-source Swedish accounting software for sole traders (enskild firma) and limited companies (aktiebolag).

License: AGPL-3.0-or-later

What is Accounted?

Accounted implements double-entry bookkeeping compliant with Swedish accounting law (Bokforingslagen). It supports the BAS 2026 chart of accounts, handles VAT declarations (momsdeklaration), SIE import/export, and enforces 7-year document retention. Built for sole traders and limited companies operating in Sweden.

Features

  • Double-entry bookkeeping -- BAS 2026 chart of accounts, draft/commit workflow, sequential voucher numbering
  • Invoicing -- Create, send, and track invoices with mixed VAT rates and PDF generation
  • Bank reconciliation -- PSD2 bank connection via Enable Banking, 4-pass automatic matching
  • VAT declaration -- SKV 4700 form mapping, per-rate breakdown, EU/export handling
  • Tax reports -- NE-bilaga, INK2, SRU export for Skatteverket
  • Supplier invoices -- Registration, payment tracking, input VAT deduction
  • Document archive -- SHA-256 integrity, 7-year retention enforcement, full archive ZIP export
  • SIE import/export -- Standard Swedish accounting interchange format
  • Extension system -- Opt-in plugins for AI categorization, receipt OCR, email, calendar, and more

Self-Hosting

git clone https://github.com/erp-mafia/gnubok.git
cd Accounted
./setup.sh              # Prompts for Supabase credentials, generates .env
docker compose up -d

You need a Supabase project and must apply the database migrations before first use. See SELF-HOSTING.md for the full step-by-step guide, including Supabase setup, auth configuration, optional features (AI, email, push notifications), and troubleshooting.

Development Setup

Prerequisites: Node.js 20+, a Supabase project.

npm install
npm run dev       # Start dev server (auto-generates extension registry)
npm test          # Run tests
npm run build     # Production build
npm run lint      # ESLint

Tech Stack

  • Framework: Next.js 16 (App Router), React 19, TypeScript (strict)
  • Database: Supabase (PostgreSQL + Row Level Security + email/password auth + TOTP MFA)
  • Styling: Tailwind CSS 4 + shadcn/ui
  • Integrations: Enable Banking (PSD2), Anthropic SDK, LangChain, OpenAI, Resend, JSZip

Documentation

  • SELF-HOSTING.md -- Full self-hosting guide (Docker, Supabase setup, migrations, optional features)
  • CLAUDE.md -- Architecture, bookkeeping engine, database conventions, extension system
  • CONTRIBUTING.md -- Development workflow, code style, pull request process
  • SECURITY.md -- Vulnerability reporting policy

Contributing

Contributions are welcome. See CONTRIBUTING.md for the full guide.

All commits require a DCO sign-off (git commit -s).

License

AGPL-3.0-or-later with an extension exception: third-party extensions that interact solely through the documented Extension API may be licensed under any terms, including proprietary. See LICENSE for details and NOTICE for third-party attributions.

S
Description
Accounted — svensk bokföringsmotor (AGPL, BAS 2026, BFL-compliant, 150+ MCP tools). Finance-kapacitet brevet ERPNext. ADR-ENGAGEMENT-001-tillägg.
Readme AGPL-3.0
56 MiB
Languages
TypeScript 93%
PLpgSQL 5.8%
JavaScript 0.4%
HTML 0.3%
MDX 0.2%
Other 0.1%