Commit Graph
7 Commits
Author SHA1 Message Date
MattssonandClaude Fable 5 526f0315d0 fix(sandbox): loop small cleanup batches (8s cap is real), clear last FK blockers (#1452)
* fix(sandbox): loop small cleanup batches (8s cap is real), clear last FK blockers

Draining the prod backlog exposed two final issues:

- The function-level statement_timeout shipped in 20260807150000 does NOT
  lift authenticator's 8s cap: the timer arms when the top-level statement
  starts (verified empirically on prod: SET LOCAL 2s canceled the RPC
  despite its 290s proconfig; matches the 2026-08-04 SIE-import finding).
  The route now loops batches of 10 (~220ms/user with the account_id
  index, so ~2.2s per batch), each rpc() call being its own statement with
  its own 8s window. The loop stops when a batch makes no progress or the
  240s time budget nears; capacity is 250 users/night.
- processing_history.company_id and invoice_deliveries.company_id are
  plain NO ACTION FKs, so sandboxes whose visitor produced AI telemetry or
  sent a demo invoice could never be deleted (7 of ~510 backlog users). A
  data-driven sweep of every NO ACTION FK into companies confirms these
  two plus the already-handled audit_log are the only such tables with
  sandbox rows. cleanup_sandbox_user (migration 20260807160000) deletes
  them explicitly; the pg fixture now seeds a processing_history row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): use valid processing_history aggregate_type/event_type in sandbox fixture

aggregate_type is CHECK-constrained and event_type is an FK to the seeded processing_event_types lookup; the guessed values failed all five fixture-dependent pg tests in CI. Validated against staging: Document/DocumentIngested inserts and tears down cleanly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox): bypass invoice-delivery delete guard in teardown, cover both blocker tables in pg fixture

CodeRabbit's fixture ask exposed a real gap: enforce_invoice_delivery_immutability silently swallows DELETEs (RETURN NULL plus a SECURITY_EVENT audit row) for terminal rows, so the explicit invoice_deliveries delete was a no-op and the companies FK still blocked teardown for sandboxes that sent a demo invoice. The trigger's DELETE branch now honors the gnubok.sandbox_cleanup flag with the same per-row sandbox re-verification as every other guard; base definition 20260803224000, all other branches untouched. The pg fixture seeds an invoice plus a marked_sent manual delivery, and a new test pins the zero-settings refusal path the Swedish review asked about. Validated on staging end-to-end.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 15:18:59 +02:00
MattssonandClaude Fable 5 bd7a423b86 fix(sandbox): fit the nightly cleanup inside PostgREST's 8s session cap (#1451)
* fix(sandbox): fit the nightly cleanup inside PostgREST's 8s session cap

Follow-up to #1449. Profiling the repaired teardown on prod puts one
sandbox user at ~3s (the auth.users delete fans out over ~250 FK triggers;
FK indexes were tried inside an aborted transaction and do not help), while
every PostgREST session inherits authenticator's statement_timeout = 8s.
The nightly RPC call therefore times out and ROLLS BACK wholesale: a second
silent-failure mode for the same cron.

- cleanup_expired_sandbox_users gets a function-local statement_timeout of
  290s (same sanctioned pattern as undo_sie_import, 20260702154500) via
  migration 20260807150000.
- The cron route bounds each night to 60 users (~180s), exports
  maxDuration = 300, and the backlog drains over a few nights.
- Tests: route asserts the bounded rpc call and the maxDuration budget; the
  pg suite pins proconfig containing statement_timeout=290s.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(db): index journal_entry_lines.account_id (chart-account cascade seq-scans 730k rows)

Caught live during the backlog purge: DELETE FROM auth.users cascades chart_of_accounts deletion, whose ON DELETE SET NULL fires an unindexed UPDATE over journal_entry_lines per account (~37 per sandbox company). This is the bulk of the ~3s per-user teardown cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 14:37:28 +02:00
MattssonandClaude Fable 5 f7f3a31f8e fix(sandbox): repair the silently-failing nightly sandbox cleanup and lock down its RPCs (#1449)
* fix(sandbox): repair the silently-failing nightly sandbox cleanup and lock down its RPCs

The daily cleanup cron has deleted nothing for months: cleanup_sandbox_user
died on the journal-line immutability trigger for every user (the seed posts
vouchers since spring), and cleanup_expired_sandbox_users swallowed each
failure as a WARNING while reporting success. 658 expired sandbox users plus
21 orphaned anonymous users had accumulated in prod auth.users.

- cleanup_sandbox_user sets the sanctioned gnubok.allow_delete flag plus a
  new transaction-local gnubok.sandbox_cleanup flag, only after verifying
  is_sandbox; write_audit_log, audit_log_immutable (DELETE only, per-row
  sandbox re-check), enforce_dimension_registry_guards (DELETE only) and
  enforce_pending_operations_no_delete (DELETE only) respect it
- clears salary_runs voucher-link FKs and purges the sandbox company's
  audit rows before the auth.users cascade
- cleanup_expired_sandbox_users returns {cleaned, failed, orphans_removed},
  additionally sweeps expired anonymous users that never got a
  company_settings row, and takes an optional p_limit for bounded batches;
  the cron route logs failures at error level and accepts both return shapes
- both RPCs lose their default PUBLIC EXECUTE grant (anon and authenticated
  could call them via PostgREST) and are now service_role-only
- validated by replaying the full delete chain against prod inside aborted
  transactions (21 users sampled across all seed eras, zero failures) and a
  committed staging run; pg-real suite + cron route unit tests added

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox): per-row sandbox re-verification in teardown guards, is_anonymous column guard

Resolution pass for PR #1449 review findings and the pg-real CI failure:

- Swedish accounting review: enforce_dimension_registry_guards and
  enforce_pending_operations_no_delete now re-verify per row that
  OLD.company_id belongs to a sandbox company (same pattern as
  audit_log_immutable) instead of trusting the gnubok.sandbox_cleanup flag
  alone. Because that re-check needs company_settings to still exist,
  cleanup_sandbox_user deletes pending_operations and dimensions explicitly
  before the auth.users cascade.
- pg-real CI: auth.users.is_anonymous does not exist in the CI
  supabase/postgres image (or on older self-hosted stacks); the orphan sweep
  in cleanup_expired_sandbox_users is now guarded on the column's existence,
  and the pg test skips the orphan assertions on such stacks.

Re-validated on staging end-to-end: {cleaned: 5, failed: 0,
orphans_removed: 1}, fresh users and non-sandbox rows untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox): make company_settings.is_sandbox write-once, prove orphan sweep fails loudly

Round-2 review findings (Swedish accounting review on PR #1449):

- Every teardown bypass trusts company_settings.is_sandbox, and RLS lets an
  owner update their own settings row via PostgREST, so a real company that
  flipped the flag would become eligible for full deletion by the nightly
  cron. New trigger makes the flag write-once (no application path updates
  it; a future sandbox-to-real conversion would ship its own migration).
- New pg test pins the reviewer's remaining concern: an anonymous user who
  somehow has bookkeeping but no company_settings row is NOT silently
  deleted by the orphan sweep; the unbypasseed immutability triggers make
  the deletion fail loudly into the summary's failed count.

Validated on staging: flip blocked in both directions, unrelated
company_settings updates unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox): guard is_sandbox provenance at INSERT, make orphan sweep exclusions explicit

Round-3 review hardening, approved by Emil:

- is_sandbox = true can now only be created by an anonymous-user JWT (the
  sandbox seed's actor), service_role, or a direct database session. A
  regular authenticated user could previously insert their settings row
  pre-flagged and have the nightly cron destroy their real books, which
  BFL 7 kap. forbids even self-inflicted. Claims are read from the
  request.jwt.* GUCs directly so the check behaves identically on hosted,
  self-hosted, and the CI auth shim.
- The orphan sweep now explicitly excludes anonymous users attached to any
  companies or company_members row, instead of relying on downstream
  immutability triggers throwing (emergent safety) to protect half-seeded
  users.
- pg tests updated accordingly: blocked/allowed provenance paths, and the
  half-seeded user is proven unreachable rather than merely failing loudly.

Validated on staging: authed insert blocked, anonymous-claim insert
allowed, half-seeded user untouched, sweep summary failed=0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sandbox): all-rows sandbox check, cleared bypass flags, tighter insert guard

CodeRabbit review pass on PR #1449 (its first non-rate-limited run):

- cleanup_sandbox_user now requires EVERY company_settings row of the user
  to be sandbox-flagged, not an arbitrary single row: a hypothetical
  mixed-company user would otherwise have their real company's rows reached
  by the user-scoped deletes.
- Both bypass flags are cleared before the RPC returns, so later work in
  the same transaction (the expired loop's next iterations, the orphan
  sweep) never runs with them still armed.
- The is_sandbox insert guard now treats ANY PostgREST claims context
  (claims json without a role claim included) as guarded, instead of
  falling open when the role claim is absent.
- The flag-leak pg test now runs inside an explicit transaction (the old
  version could not observe transaction-local GUCs at all), and a new test
  covers the mixed sandbox/real user refusal.

Declined: replacing the em dashes inside the two replicated Swedish
exception messages; they are byte-identical copies of the strings already
deployed by migration 20260702084500 and changing them would alter live
user-facing errors out of scope.

Validated on staging: mixed user refused, flags cleared post-teardown,
role-less claims blocked.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-07 13:51:01 +02:00
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
MattssonandClaude Opus 4.7 5725c25bf1 Logs/improved logging (#398)
* feat(mcp): add create_transactions tool with /pending approval gate

New MCP tool gnubok_create_transactions stages 1–10 transactions per call
as pending_operations of type create_transaction (risk: medium). Each item
becomes its own card on /pending; on confirm, the executor inserts the row
into transactions with import_source='mcp' so MCP-staged ingestion is
distinguishable from PSD2 sync. Designed for skill workflows that pull
external data (e.g., Airtable) and want the user to gate the writes.

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

* fix(bas): strip concatenated group headers from corrupted account names

A chart-data import bug had glued the next group's header onto the last
account in each preceding group across all eight bas-data class files
(e.g. account 2670 read "Utgående moms på försäljning inom EU, OSS 27
PERSONALENS SKATTER, AVGIFTER OCH LÖNEAVDRAG"). The corrupted names
surface in transaction dropdowns, ledgers, SIE exports and årsredovisning,
and risk VAT miscategorization on the OSS (2670) and blandad-verksamhet
(6999) accounts specifically.

- Cleans 69 account_name and 64 description fields across class-1..8 files
- Adds a regression test asserting no name contains a concatenated header
- Ships an idempotent safety-net migration that updates already-seeded
  chart_of_accounts rows, gated on the corrupted string so user
  customizations are preserved

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

* feat(errors): add structured error codes and handling for various operations

- Introduced a new structured error registry in `structured-errors.ts` to standardize error handling across the application.
- Added Swedish and English messages for various error scenarios, including validation, authorization, and bookkeeping errors.
- Implemented a client-side error toast in `use-error-toast.ts` to display user-friendly error messages with remediation hints.
- Created a wrapper for recording operation outcomes in `record-operation.ts`, enhancing audit capabilities for operations.
- Developed a provider call wrapper in `with-provider-call.ts` to handle external HTTP calls with structured logging and error mapping.
- Added a new SQL migration to extend the processing history with new event types and aggregate types for better operational telemetry.

* Refactor supplier API routes to use context-based logging and error handling

- Replaced direct Supabase client usage in GET and POST routes with context-based approach using `withRouteContext`.
- Enhanced error handling to provide structured error responses for supplier creation and listing.
- Updated logging to include request IDs for better traceability.
- Introduced new error codes for supplier-related operations.
- Refactored tax deadlines cron job to utilize context and improved error handling.
- Updated ESLint configuration to enforce logging practices across API and lib directories.
- Enhanced arcim migration extension with structured error handling and logging.
- Added classification for provider errors to improve user-facing error messages.
- Introduced request ID in extension context for better log correlation.

* fix(route-context): update DynamicParams type for improved type safety in route handlers

* feat(transactions): add 'create_transaction' operation to PendingOperationType

* fix(route): ensure companyId is non-nullable in loadAndDeriveAbsence function

* fix(route-context): ensure companyId is always non-null by short-circuiting with COMPANY_CONTEXT_MISSING

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:12:02 +02:00
MattssonandClaude Opus 4.6 550cadcb06 fix: harden auth, cron secrets, and provider flows (GNU-17) (#148)
- Replace === with crypto.timingSafeEqual in all 7 cron routes via shared lib/auth/cron.ts
- Add in-memory rate limiting (60 req/min) and expires_at support to calendar feed
- Add exponential backoff on MFA verify after 3 failed attempts
- Add 60s cooldown on password reset requests
- Validate bank callback auth code format before API call
- Redact session IDs from bank sync and callback logs
- Validate OAuth redirect_uris against allowlist (claude.ai, claude.com, localhost)
- Remove excessive PII/debug console logging from login page

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-29 23:49:09 +02:00
109f860e22 Sandbox (#8)
* feat: add sandbox infrastructure — migration, types, and middleware

Add database migration for sandbox support:
- Add `is_sandbox` boolean column to company_settings
- Update 4 enforcement trigger functions (journal entry immutability,
  journal entry line immutability, retention enforcement, document
  deletion blocking) to bypass checks for sandbox users
- Add `cleanup_sandbox_user()` SECURITY DEFINER function that handles
  FK-safe deletion order (document_attachments → journal_entry_lines →
  journal_entries → supplier_invoices → auth.users cascade)
- Add `cleanup_expired_sandbox_users()` function that loops over
  sandbox users older than N hours with per-user error handling

Update TypeScript types:
- Add `is_sandbox: boolean` to CompanySettings interface
- Add `is_sandbox: false` to makeCompanySettings() test factory

Update middleware:
- Add `/sandbox` to public routes so the landing page is accessible
  without authentication

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add sandbox landing page, seed API, cleanup cron, and banner

Sandbox landing page (app/sandbox/page.tsx):
- Client component matching the existing auth page aesthetic
- Auth check: if logged in as real user, shows message to use incognito
- Otherwise shows feature overview (invoices, transactions, bookkeeping,
  reports) with "Starta sandbox" button
- On click: signInAnonymously() → POST /api/sandbox/seed → redirect
- Uses window.location.href for full page load (ensures middleware
  picks up new session cookies)

Seed API (app/api/sandbox/seed/route.ts):
- POST handler gated to anonymous users only (403 for real users)
- Idempotent: returns { seeded: false } if company_settings exists
- Seeds ~40 rows: profile, company_settings (is_sandbox: true,
  onboarding_complete: true), chart of accounts (via RPC),
  fiscal period, 3 customers (Swedish business, EU business,
  individual), 4 invoices (paid/sent/overdue/draft), 4 invoice
  items, 2 posted journal entries with 5 lines, 8 transactions
  (3 categorized, 2 income, 3 uncategorized), 2 deadlines
- Journal entries inserted directly (not via engine) to avoid
  event emission, using next_voucher_number() RPC

Cleanup cron (app/api/sandbox/cleanup/cron/route.ts):
- GET handler with CRON_SECRET Bearer token auth
- Creates service role Supabase client
- Calls cleanup_expired_sandbox_users RPC (24h default)

Sandbox banner (components/dashboard/SandboxBanner.tsx):
- Amber bar with dismiss button (client state, reappears on reload)
- Text: "Sandlådemiljö — dina data raderas automatiskt efter 24 timmar"
- "Skapa konto" link to /register

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: integrate sandbox into dashboard — banner, nav, settings safeguards

Dashboard layout (app/(dashboard)/layout.tsx):
- Fetch is_sandbox from company_settings
- Render SandboxBanner at top of page for sandbox users
- Pass isSandbox prop to DashboardNav
- Hide RecaptIdentify analytics for sandbox users

Root page (app/page.tsx):
- Same sandbox banner and isSandbox prop treatment as dashboard layout
  (root page has its own layout, not wrapped by (dashboard)/layout)

DashboardNav (components/dashboard/DashboardNav.tsx):
- Add optional isSandbox prop
- Change logout button text to "Avsluta sandbox" when isSandbox
- Redirect to /sandbox instead of /login on logout for sandbox users
- Applied to both desktop sidebar and mobile drawer logout buttons

Settings page (app/(dashboard)/settings/page.tsx):
- Hide "Bank (PSD2)" tab entirely for sandbox users — prevents
  connecting real bank accounts from a temporary anonymous session
- Hide "Radera konto" card for sandbox users — account auto-deletes
  via cron, and the delete flow requires email confirmation

Vercel config (vercel.json):
- Add sandbox cleanup cron at 04:00 UTC daily
  (/api/sandbox/cleanup/cron)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove audit trigger for non-existent tax_codes table

Migration 018 referenced public.tax_codes which was never created
(migration 012 is a placeholder). This caused failures when running
migrations from scratch on a fresh database.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove ALTER FUNCTION for 3 non-existent functions

Removed search_path pinning for create_invoice_with_items,
seed_asset_categories, and update_reconciliation_session_counts —
none of these functions were ever created in any migration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove ALTER for generate_invoice_number (created in later migration)

The function is created in migration 20260306 with search_path already
set, but migration 20260304 tried to ALTER it before it existed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Fixed redirect issue

* Update app/api/sandbox/seed/route.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update app/api/sandbox/seed/route.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Update app/sandbox/page.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* Fixed catch block issue

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-11 14:54:53 +01:00