Commit Graph

21 Commits

Author SHA1 Message Date
Jakob Wennberg dc5aea4a35 feat(mcp): speak spec revision 2026-07-28 (stateless core) (#1277)
* feat(mcp): speak spec revision 2026-07-28 (stateless core)

Adopt the 2026-07-28 MCP spec revision on the connector endpoint while
keeping every handshake-era client (2025-06-18 and earlier) byte-identical:

- Accept per-request _meta protocol negotiation
  (io.modelcontextprotocol/protocolVersion); unsupported versions return
  UnsupportedProtocolVersionError (-32022) with the supported list.
- Implement server/discover (spec MUST): supported revisions, capabilities
  including the extensions field, identity, instructions, freshness hints.
- Decorate results for stateless clients: required resultType, serverInfo
  in _meta, and CacheableResult ttlMs/cacheScope on tools/list,
  prompts/list, resources/list, resources/read.
- Validate the standard Mcp-Method/Mcp-Name request headers when present
  (HeaderMismatchError -32020); absence stays accepted.
- Declare the ratified MCP Apps extension (io.modelcontextprotocol/ui) in
  capabilities; the widgets already use the ratified mime type and
  _meta.ui.resourceUri shape, so no widget changes are needed.
- OAuth: include the RFC 9207 iss parameter on every authorization
  response (success and error) and advertise
  authorization_response_iss_parameter_supported in RFC 8414 metadata.

Resource-not-found already used -32602 and tools/list ordering was already
deterministic; both are covered by the new test file.

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

* fix(mcp): Mcp-Name covers params.uri, base64 sentinel, version-header consistency

Review follow-ups against the transport spec text: Mcp-Name mirrors
params.name OR params.uri (resources/read), values arrive base64-wrapped
in the =?base64?...?= sentinel and must be decoded before comparison, and
an MCP-Protocol-Version header that disagrees with the _meta protocol
version is a HeaderMismatch. Absence of any header stays accepted since
this server supports handshake-era clients (spec-sanctioned leniency).

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-29 18:02:05 +02:00
Jakob Wennberg 7dde8cac82 fix(security): resolve the CodeQL backlog, three fixes and three documented false positives (#1225)
Triage of all 9 CodeQL alerts surfaced on main by #1223. None were introduced by that PR.

Fixed: the compliance-review artifact now unpacks to runner.temp instead of over the trusted checkout (actions/artifact-poisoning, critical); MCP LIKE patterns escape backslash first, which was a real correctness bug returning wrong rows for any search containing a backslash (js/incomplete-sanitization, 2 sites); and the mcp-oauth consent form action is HTML-escaped (js/reflected-xss, not exploitable because WHATWG URL already percent-encodes " < >, but & is not in that encode set).

Dismissed as false positives with reasoning recorded at each site and in DECISIONS.md: sie-export escapeQuotes, where doubling backslashes would violate SIE 4B, corrupt files in conformant readers and skew #KSUMMA under BFL 7-year retention; hashApiKey, where SHA-256 is correct for a 256-bit CSPRNG token and changing it would invalidate every live gnubok_sk_ key; and the DuplicateBookingDialog href, which is a DB UUID behind a literal path prefix.

Regression tests cover both behavioural fixes, including the escape ordering.
2026-07-27 14:02:25 +02:00
Mattsson abe9ac9d8c Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots

setup:extensions and vitest write these files with LF; with
core.autocrlf=true git expects CRLF and flags them as phantom
modifications on every dev/build run.

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

* fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route

mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt.

Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }.

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

* fix(api): route transactions endpoints through withRouteContext

Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern.

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

* fix(api): route SIE import and bank reconciliation through withRouteContext

Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern.

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

* fix(api): route salary endpoints through withRouteContext

Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern.

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

* fix(api): route report endpoints through withRouteContext

Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern.

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

* fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext

Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated.

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

* fix(api): route documents, events, team and account endpoints through withRouteContext

Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated.

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

* fix(api): route settings and pending-operations endpoints through withRouteContext

Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern.

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

* chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration

Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md.

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

* feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil"

Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by
upload instead of typing every ruta into the form. Extract buildFiledAmounts()
as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so
the XML file and the manual-filing PDF can never disagree. Adds the /eskd API
route, an XML option in the report export menu, and the upload button on the
manual-filing card. Strings in sv + en.

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

* feat(vat): add 'vat_settlement' source type and update related components

* fix(booking): adjust search input layout and enable autofocus

* fix(vat): support 12-digit org numbers and adjust emission order for eSKD file

* fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:54:46 +02:00
Jakob Wennberg 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
Jakob Wennberg 5078b4e02d fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710) (#714)
* fix(mcp): grace window + idempotent refresh-token replay for OAuth (#710)

OAuth refresh rotated BOTH the refresh token and the access key in one
zero-grace CAS. Claude Code's MCP OAuth client fails to persist the rotated
refresh token (or fires concurrent refreshes), re-presents the stale one, the
CAS matches 0 rows, and the grant dies with invalid_grant — forcing a full
re-authorization roughly every 60s in a loop. Regression from #392.

Keep rotation (RFC 9700 §4.14.2 requires it for public clients) but add a
bounded grace window with idempotent replay, atomic in one SECURITY DEFINER RPC:

- Migration adds previous_key_hash / previous_refresh_token_hash (+ *_expires_at)
  shadow columns. validate_and_increment_api_key accepts the current OR an
  unexpired previous key_hash, with the rate-limit increment keyed off the
  resolved row id.
- New rotate_mcp_refresh_token RPC: rotated | replayed | reuse_revoked |
  revoked | invalid. In-grace replay re-issues a fresh pair and slides the
  window so an actively-refreshing client that cannot persist the rotated token
  keeps working; reuse after the window revokes the grant family (RFC 9700
  4.14.2 reuse detection preserved).
- The refresh grant now calls the one RPC, closing the old SELECT-then-CAS
  TOCTOU gap.

All previous_* columns default NULL, so existing keys are unaffected and the
RPC return shape is unchanged (callers untouched).

Tests: rewired the token-route unit tests to the RPC and replaced the test that
codified the bug with a #710 regression (in-grace replay returns 200, not 400);
added tests/pg/mcp-oauth-rotation-grace.pg.test.ts (grace accept/expire,
revoke-never-graced, rotate->demote, idempotent replay, reuse-after-grace->revoke).

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

* ci: retrigger checks for #714

No code change — re-running CI. The Supabase Preview check fails on a
pre-existing main-branch migration-history drift ("Remote migration versions
not found in local migrations directory"), not this PR; pg-real (full migration
replay) passes.

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-12 10:05:10 +02:00
Mattsson e4d4d8e4dc feat: enhance OAuth scopes and UI for agent-driven approval process (#544)
* feat: enhance OAuth scopes and UI for agent-driven approval process

* refactor: update OAuth scopes to enforce explicit user consent for write and approval actions
2026-05-20 13:39:02 +02:00
Mattsson 566ed72984 Bug/mcp connection issue (#541)
* feat(api): implement caching and logging in health check endpoint

- Added in-memory caching for health check responses to reduce load on Postgres.
- Introduced logging for error handling in health check.
- Updated response structure to exclude error details from public responses.

feat(api): enhance OAuth consent UI and scope handling

- Improved consent UI to reflect exact requested scopes and added better user guidance.
- Updated scope handling logic to ensure least-privilege access.
- Enhanced styling for better user experience and accessibility.

chore(docker): improve security and resource management in Docker setup

- Updated Docker Compose configuration to enforce read-only file systems and resource limits.
- Added health checks and logging options for better observability.
- Introduced optional Caddy reverse proxy for TLS termination.

fix(migrations): resolve ambiguity in create_company_with_owner function

- Dropped orphaned 3-arg overload of create_company_with_owner function.
- Recreated canonical 4-arg version with cash account seeding logic.
- Ensured proper permissions for function execution in Postgres.

* feat: enhance security checks for team membership in company creation

* test: add CSP tests for OAuth authorization endpoint

* feat: enhance error handling and reporting in bank file import process
2026-05-20 10:08:32 +02:00
Mattsson 9aced4790c feat(api): implement caching and logging in health check endpoint (#526)
* feat(api): implement caching and logging in health check endpoint

- Added in-memory caching for health check responses to reduce load on Postgres.
- Introduced logging for error handling in health check.
- Updated response structure to exclude error details from public responses.

feat(api): enhance OAuth consent UI and scope handling

- Improved consent UI to reflect exact requested scopes and added better user guidance.
- Updated scope handling logic to ensure least-privilege access.
- Enhanced styling for better user experience and accessibility.

chore(docker): improve security and resource management in Docker setup

- Updated Docker Compose configuration to enforce read-only file systems and resource limits.
- Added health checks and logging options for better observability.
- Introduced optional Caddy reverse proxy for TLS termination.

fix(migrations): resolve ambiguity in create_company_with_owner function

- Dropped orphaned 3-arg overload of create_company_with_owner function.
- Recreated canonical 4-arg version with cash account seeding logic.
- Ensured proper permissions for function execution in Postgres.

* feat: enhance security checks for team membership in company creation
2026-05-19 17:52:11 +02:00
Mattsson 8a6ce7093e feat: implement skattekonto drift detection and alerting (#525)
* feat: implement skattekonto drift detection and alerting

- Add skattekonto drift computation logic to compare Skatteverket's saldo with GL 1630 sum.
- Implement alerting mechanism for significant drift changes, with throttling to prevent alert spamming.
- Introduce database functions to sum GL 1630 entries and list unbooked skattekonto rows.

feat: create own account transfer detection

- Develop logic to detect transfers between a company's own cash accounts based on counterparty IBAN.
- Implement tests to validate detection logic under various scenarios, including matching and non-matching IBANs.

feat: establish cash accounts as a first-class entity

- Create cash_accounts table to manage routable cash accounts, replacing ad-hoc JSONB structures.
- Implement functions for listing, upserting, and managing cash accounts, including primary account designation.

feat: enhance GL line reconciliation functionality

- Modify get_unlinked_1930_lines RPC to accept any account number for reconciliation, improving flexibility for different currencies.
- Update related functions to ensure compatibility with the new cash_accounts structure.

feat: capture counterparty IBAN in transactions

- Add counterparty_iban column to transactions table to facilitate intra-account transfer detection.
- Create index for efficient lookups based on counterparty IBAN.

* feat: Enhance cash account handling and reconciliation processes

- Updated reconciliation routes to enforce cash account validation for all account numbers, including '1930'.
- Improved error handling for unknown cash accounts in reconciliation status and unmatched entries routes.
- Changed CashAccountSelector to use sessionStorage instead of localStorage for better data privacy.
- Fixed mapping for employer payroll taxes to route to the correct account (2730 instead of 2731).
- Added safety checks for company IDs in the guessCounterAccount function to prevent injection vulnerabilities.
- Introduced atomic RPC for setting primary cash accounts to avoid intermediate states during updates.
- Seeded default cash accounts for new companies to ensure reconciliation routes are accessible from day one.
- Updated email notifications for drift detection to avoid exposing sensitive financial data.
- Enhanced bank reconciliation logic to handle multi-currency transactions correctly.
- Renamed and updated tests to reflect changes in the underlying RPCs and ensure accurate coverage.
- Migrated existing cash account rules to correct mappings in compliance with Swedish accounting standards.
2026-05-19 16:10:18 +02:00
Mattsson 16164ea14c Fix/mcp fixes and bugs (#518)
* feat(voucher): add create voucher and correct entry previews; update commit methods

* feat: add support for pending operations in API key scopes and OAuth client management

- Introduced new API key scopes for reading and approving pending operations.
- Updated the scope groups to include pending operations.
- Added new tools for listing and managing pending operations.
- Implemented OAuth client registration and revocation endpoints.
- Created a UI panel for managing OAuth clients, including registration and revocation.
- Added tests for pending operations tools and OAuth allowlist functionality.
- Implemented a database migration for OAuth client registrations with appropriate policies and constraints.

* feat: Implement OAuth client registration rate limiting and enhance security measures

- Added IP-based rate limiting to the OAuth client registration endpoint to prevent enumeration attacks.
- Introduced a service-role client for allowlist lookups, ensuring trust boundaries are maintained.
- Updated error responses to be uniform across different types of redirect URI validation failures.
- Enhanced tests to reflect changes in OAuth scope handling, ensuring fallback to read-only scopes when no scopes are provided.
- Improved handling of high-risk pending operations, requiring explicit confirmation for approvals.
- Added audit logging for OAuth client revocations and pending operation approvals/rejections to maintain a security audit trail.
- Refactored API key scope management to include default read-only scopes for OAuth-issued keys and added segregation-of-duties checks.
2026-05-18 19:02:42 +02:00
Mattsson 3fa871c742 Bug/accounting suggestion (#456)
* feat: add bike benefit handling and optional vacation accrual

- Introduced bike benefit (cykelförmån) with calculations for annual market value and monthly taxable value.
- Updated schemas to include new benefit types and validation rules.
- Implemented API routes for creating, updating, and deleting employee benefits.
- Enhanced salary calculation logic to accommodate new vacation rule options, including a 'none' option for no accrual.
- Added UI components for managing employee benefits, including input for bike benefit specifics.
- Created database migrations for employee benefits and updated salary line items to support new benefit types.

* chore: remove Langfuse env var checks

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

* feat: enhance OAuth callback URL handling and update default scopes for Visma integration

* feat: remove trade_name field and simplify company naming in invoices

* refactor: destructure canWrite from useCanWrite for consistency across components

* feat: enhance PATCH endpoint to validate existing benefits and handle bike benefit updates

* feat: add missing label for bike benefit in salary line item types

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 00:26:04 +02:00
Mattsson c03582b5c7 Fix/percistent mcp connection (#392)
* feat(oauth): add support for refresh tokens in OAuth flow and update database schema

* feat(prompts): add MCP prompts and corresponding functionality for prompt retrieval

* feat(auth): enhance error handling for refresh token operations and validation
2026-05-05 13:48:53 +02:00
Mattsson 064fb7f7a9 Add/white label (#381)
* feat(branding): add BrandingService with default-preserving env layer

Introduce lib/branding/service.ts mirroring lib/email/service.ts. Defaults
match current gnubok values exactly, so production behaviour is unchanged
unless an env var (NEXT_PUBLIC_BRANDING_*, BRANDING_*) or extension override
(via registerBrandingService) is set.

Resolution order: defaults < env vars < extension override.

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

* feat(branding): route root layout, manifest, and PWA assets through branding service

- app/layout.tsx now reads title, description, themeColor, and apple-touch-icon
  from getBranding() instead of hardcoded values.
- public/manifest.json replaced by dynamic app/manifest.ts so PWA name,
  short_name, description, theme_color, background_color, and icon paths
  are resolved at request time.

The manifest now serves at /manifest.webmanifest (Next.js convention for
the metadata file route). The previous /manifest.json URL is no longer
populated; nothing in core references it after this commit.

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

* feat(branding): route email service and templates through branding service

- resend-service.ts: From line uses getBranding().appName instead of
  hardcoded "Gnubok" in both the with-fromName and bare cases.
- invite-templates.ts: subject, HTML header, body, plain text, and the
  team-invite variants all read from branding (sentence case in prose,
  uppercased for the styled <p> header).
- consent-notification-templates.ts: signature fallback (companyName ||
  branding) for both HTML and plain text variants.

Defaults preserve the exact current strings ("Gnubok", "GNUBOK", "gnubok"
in their respective contexts) so no email content changes for production.

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

* feat(branding): route OAuth consent page through branding service

The MCP OAuth consent page rendered for Claude Desktop / Claude.ai
connector flows now reads the app name from getBranding() for both the
HTML <title> and the body copy. Default still produces "gnubok" in
lowercase prose, matching current behaviour.

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

* feat(branding): route auth, dashboard, and onboarding text through branding service

Replace user-visible "gnubok" / "Gnubok" references with calls to
getBranding(). Touches:

- Auth pages (login, register, mfa/enroll): logo src/alt, MFA TOTP
  friendlyName.
- Onboarding (companies/new, invite, sandbox, WelcomeOnboarding,
  Step2CompanyDetails, NewUserChecklist, BankIdCompanyPicker,
  ArcimMigrationWorkspace): logo, headings, error/help text.
- Dashboard fallback (companyName="gnubok") and settings (backup copy,
  ApiKeysPanel MCP connector name + login note, CompanyDangerZone,
  retention-notice).
- API routes (support contact subject prefix, enable-banking consent
  email companyName fallback, AI inbox receipt-request appUrl,
  pain001 messageId prefix).
- MCP server "open the gnubok web app" review message.
- Salary/reports filings (AGI Programnamn, KU10 Programnamn,
  payslip footer, full-archive system metadata, SRU #PROGRAM line).

Internal identifiers (cookie names gnubok-company-id /
gnubok-invite-token, API key prefix gnubok_sk_, invite token prefix
gnubok_inv_, MCP tool names, npm package gnubok-mcp, GNUBOK_API_KEY
env name) are deliberately left unchanged — they're stable contracts
that whitelabels must not break.

Defaults match current behaviour exactly.

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

* feat(branding): support legal page field-level swaps for entity and contact

Privacy and DPA pages now interpolate appName, legalEntity, and
privacyEmail from the branding service instead of hardcoding "Gnubok",
"Arcim", and "privacy@gnubok.se". Page metadata uses generateMetadata()
so titles also reflect the brand.

lib/support.ts now falls back to getBranding().supportEmail when
SUPPORT_RECIPIENT_EMAIL is unset, so a single BRANDING_SUPPORT_EMAIL
env var configures both the support form recipient and the displayed
support address.

Whitelabels with a different legal jurisdiction or entirely different
DPA text should override the page route from an extension. Phase 1
intentionally only supports field-level swaps.

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

* docs(branding): add WHITELABEL.md and example branding extension

WHITELABEL.md: fork checklist, env var reference, the "do not change"
list (cookies, API key prefixes, invite token prefixes, MCP tool names,
gnubok-mcp npm package, GNUBOK_API_KEY env name), out-of-scope items,
the upstream sync workflow YAML to copy into a fork, conflict avoidance
guidance, and a verification checklist.

extensions/general/_example-branding/: copy-paste starter extension with
index.ts (commented placeholder values for registerBrandingService),
manifest.json, and README.md. Disabled by default (not added to
extensions.config.json); whitelabels cp the folder, edit, and enable.

sectors.test.ts: bumped expected extension count 12 -> 13 to account
for the new starter extension on disk. The generated registry is
unchanged because the example is disabled.

The sync workflow YAML is documented inline in WHITELABEL.md rather
than checked in as a workflow file. It's only meaningful in a fork --
gnubok itself has nothing to sync from.

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

* fix(branding): address PR review — lazy support email + escape brand in HTML/XML

Three issues from code review:

P1 — lib/support.ts: SUPPORT_RECIPIENT_EMAIL was a module-level const,
evaluated at import time before extensions register branding overrides
via ensureInitialized(). Convert to getSupportRecipientEmail() lazy
accessor; update the only caller in app/api/support/contact/route.ts.
Extension-supplied supportEmail values now route correctly.

P2 — app/api/mcp-oauth/authorize/route.ts: appName was interpolated
into the consent page HTML without escapeHtml(), inconsistent with
the existing escaping of companyName. Wrap appName.toLowerCase() in
escapeHtml() at use sites in <title> and the body paragraph.

P2 — lib/salary/agi/xml-generator.ts and lib/salary/ku/ku10-generator.ts:
appName placed inside <gem:Programnamn> / <Programnamn> XML elements
without escapeXml(), the helper already used for other admin-controlled
fields in the same files. Wrap accordingly to prevent malformed XML if
a brand name contains XML reserved characters.

All admin-controlled inputs only — no user-exploitable path. Defense in
depth, not a known incident.

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

* fix(branding): security follow-up — lazy metadata, SRU/email header sanitization

Self-audit after the PR review surfaced four more concerns. Fixes them
with the same defense-in-depth posture as the prior review fixes.

1. app/layout.tsx — same eager-evaluation class as P1 support.ts. The
   module-level `const branding = getBranding()` froze branding before
   extensions registered, so extension-based overrides for title,
   description, themeColor, and apple-touch-icon silently never applied.
   - Convert to generateMetadata() / generateViewport() (lazy, run per
     request, see extension-registered overrides).
   - Inline getBranding() inside RootLayout for the apple-touch-icon
     href so it picks up overrides too.
   - Add ensureInitialized() at module level so extensions are loaded
     before the first metadata call. Mirrors the API route pattern.

2. app/manifest.ts — same class. The dynamic manifest function reads
   getBranding() per request, but if the manifest is requested before
   any other module has triggered ensureInitialized(), extensions are
   still unloaded. Add ensureInitialized() at module level.

3. lib/reports/ink2/sru-generator.ts — appName interpolated into the
   SRU `#PROGRAM` directive without sanitization. SRU's reserved char
   is `#` (directive marker) and CRLF injects new directives. Wrap in
   the existing sanitizeString() helper to match the pattern used for
   other admin-controlled fields in this file (#NAMN, #ADRESS, etc.).

4. extensions/general/email/lib/resend-service.ts — appName and the
   user-controlled fromName both flow into the From header. Resend's
   API does its own validation, but defense in depth: strip CRLF and
   angle brackets via a small sanitizeHeaderPart() helper before
   building the header string. fromName was a pre-existing surface;
   appName is new with this whitelabel work.

All four are admin-controlled inputs (env vars or extension code),
not user-exploitable. No known incidents — defense in depth, and
correctness for extension-based whitelabels.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:32:26 +02:00
Mattsson bb336eba88 Fix/user feedback (#210)
* Add delete policies for provider consent tokens and provider OTC

* Add trade name support for companies in settings and documents

* Resolved currency selection issue

* Enhance invoice line display with foreign currency support and update delivery date schema to allow empty values

* Add currency display for journal entries and include currency metadata in transaction creation

* Add trade_name column to company_settings for external display
2026-04-09 16:22:51 +02:00
Jakob Wennberg e2d9c8bb85 fix: MCP OAuth 303 redirect, send dialog auto-close, bank details null payload (#175)
* fix: MCP OAuth 303 redirect, send dialog auto-close, bank details null payload

- OAuth authorize: use 303 See Other instead of default 307, which
  preserved POST method and caused Claude's callback to return 405
- SendInvoiceDialog: close dialog and show toast after email send
  instead of leaving a success message that requires manual close
- BankDetailsSetupDialog: omit empty fields from payload instead of
  sending null, which fails Zod validation on non-nullable schema fields

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

* refactor: remove dead sentMessage state and fix stale comment

Remove sentMessage state, its success banner JSX, and the CheckCircle2
import — all unreachable after the dialog now auto-closes on email send.
Fix stale "to null" comment in BankDetailsSetupDialog.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 10:27:40 +02:00
Mattsson 0dd1f5ebc1 feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19)

Introduce companies table, company_members, and user_preferences to
support multiple companies per user. All data scoping changes from
user_id to company_id across the entire codebase.

Key changes:
- Database migration: new tables, company_id on 40+ tables, backfill,
  RLS rewrite from user_id to company-member-based, updated RPCs
- Types: Company, CompanyMember, CompanyRole, UserPreferences types;
  company_id added to all entity interfaces; companyId on all events
- Engine: all 7 core functions take companyId; storno, period, year-end
  services updated; 16 report generators updated
- Middleware: company context resolution (cookie → prefs → first company)
- API routes: ~120 routes updated with requireCompanyId()
- Frontend: CompanyProvider context, layout/dashboard/onboarding updated
- Extensions: context factory, 9 extensions, all lib files updated
- Tests: 1880 tests passing, all helpers updated with company_id defaults

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

* feat: add database migrations for multi-tenant company and team system (GNU-19)

Adds company_invitations, company creation RPC, team_members, account
deletion RPC, and teams table refactor migrations. Updates base
multi-tenant migration with cascading FKs and onboarding_step column.

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

* feat: add team types and update core infrastructure for multi-tenancy (GNU-19)

Adds TeamRole, MemberSource, and Team types. Refactors Supabase service
client to be stateless, updates middleware for team-aware routing, extends
CompanyContext with team/role fields, and updates extension service types
to accept companyId.

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

* refactor: thread company_id through business logic functions (GNU-19)

Replaces user_id scoping with company_id across all lib modules:
bookkeeping, documents, transactions, invoices, reconciliation, tax,
deadlines, and import. Updates corresponding tests.

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

* refactor: thread company_id through API routes and extensions (GNU-19)

Updates all existing API routes to extract and pass companyId. Updates
enable-banking and arcim-migration extensions for company-scoped
transaction ingestion and sync.

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

* feat: add company and team management API routes (GNU-19)

Adds CRUD endpoints for company members, company invitations, team
members, and team invitations. Includes invite token utilities, email
templates, and company switch server action.

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

* feat: add team/company UI components, pages, and dashboard updates (GNU-19)

Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company
members and team management panels. Updates dashboard layout for
team-aware routing, onboarding for multi-step role choice, and auth
callback for team invite acceptance. Ignores supabase/.branches/.

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

* fix: add null guards for company in import page (GNU-19)

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

* fix: move appUrl declaration to outer scope in invite route (GNU-19)

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

* fix: add optional chaining for company.name in members section (GNU-19)

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

* fix: add optional chaining for second company.name in members section (GNU-19)

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

* fix: add null guards for company in extension components (GNU-19)

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

* fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19)

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

* fix: update tests to use companyId instead of userId and improve type handling

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:41:52 +02:00
Mattsson 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
Mattsson bb473e2c57 Worktree api key scopes (#139)
* feat: add read/write scopes to API keys

API keys now require explicit scopes (e.g. transactions:read,
invoices:write) instead of having implicit full access. The create
dialog shows grouped checkboxes per domain with read/write split.
Legacy keys with null scopes default to read-only. MCP tools/list
is filtered by scope and tools/call rejects unauthorized calls.

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

* Enhance API key scopes with suppliers and update descriptions for better clarity

* fix: drop function before recreating with changed return type

PostgreSQL cannot change return type via CREATE OR REPLACE.
Drop the existing function first to avoid SQLSTATE 42P13.

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

* fix: add new migration to drop and recreate function with scopes return type

The original migration was already applied, so a new migration is needed
to DROP the function first before recreating with the updated return type.

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

* fix: add DROP FUNCTION to original migration, remove redundant fix migration

Preview branches replay all migrations from scratch. The original migration
must DROP the function before recreating it with a changed return type,
otherwise PostgreSQL rejects the CREATE OR REPLACE. The separate fix
migration is no longer needed.

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

* fix: rename migration to avoid duplicate version in schema_migrations

Version 20260325120000 is already recorded in the preview DB from a
prior failed apply. Renaming to 20260326130000 so Supabase treats it
as a new migration.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 13:30:26 +01:00
Jakob Wennberg 51467717f1 fix: broaden OAuth redirect_uri allowlist for Claude Desktop (#77)
* fix: broaden OAuth redirect_uri allowlist for Claude Desktop

Claude Desktop uses varying callback paths (with org/connector IDs),
not a single fixed callback URL. Allow any path under claude.ai and
claude.com instead of requiring an exact match.

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

* fix: tighten redirect_uri allowlist to /api/ prefix only

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 15:52:00 +01:00
Jakob Wennberg d61670231f fix: plain 401 for MCP OAuth discovery (#74)
* fix: return plain 401 for MCP OAuth discovery and harden auth flow

- Return plain HTTP 401 with WWW-Authenticate header (no JSON-RPC body)
  so Claude Desktop's MCP client can trigger OAuth discovery correctly
- Remove unused apiKey import from authorize route
- Remove stale codeChallengeMethod parameter from oauth-codes

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

* fix: add Retry-After header to 429 rate limit response

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 14:47:47 +01:00
Jakob Wennberg 5d66dd6bfc feat: MCP server, API keys, OAuth, and KPI dashboard (#72)
* fix: prevent Chrome auto-translate from crashing React during onboarding

Chrome auto-translate modifies DOM text nodes when it detects a Swedish
page (lang="sv") in a browser set to English. React does not expect
external DOM mutations and throws, crashing the entire component tree
into global-error.tsx on every step transition.

Add translate="no" and <meta name="google" content="notranslate"> to
suppress browser translation. Also fix timezone-unsafe date parsing in
fiscal period validation (new Date("YYYY-MM-DD") + getDate() returns
local-timezone values, shifting dates by -1 day in Western timezones).

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

* fix: add notranslate meta tag to global-error.tsx for consistency

Per review feedback — global-error.tsx renders its own <html> document,
so it needs the same <meta name="google" content="notranslate"> tag as
layout.tsx to fully suppress Chrome translation on error pages.

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

* feat: add MCP server extension with OAuth, API keys, and KPI dashboard

Let users do bookkeeping through Claude Desktop, Claude Code, or any
MCP-compatible client. "Show my uncategorized transactions." "Book that
as office supplies." "Invoice Acme for 15,000 kr."

MCP server (extension):
- 10 tools: transactions, categorization, customers, invoices,
  trial balance, VAT report, KPI report, income statement
- JSON-RPC 2.0 protocol (no SDK dependency, works in serverless)
- Tool annotations, pagination, input validation per MCP best practices
- Same engine as web UI (VAT rules, exchange rates, event emission)

API key infrastructure (core):
- api_keys table with RLS, rate limiting (100 RPM), scopes column
- Atomic rate limit via DB RPC (validate_and_increment_api_key)
- Key management API routes + settings UI panel

OAuth 2.1 for Claude Desktop connectors:
- .well-known/oauth-protected-resource + oauth-authorization-server
- Authorization endpoint with consent page
- Token endpoint with PKCE verification
- Stateless encrypted auth codes (AES-256-GCM, no DB storage)
- Dynamic client registration

KPI dashboard:
- /nyckeltal page with hero cards, operational grid, trend chart
- GET /api/reports/kpi endpoint
- Gross margin, cash position, expense ratio, avg payment days,
  VAT liability, revenue/expense trend

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

* fix: address OAuth security vulnerabilities from code review

Critical fixes:
- Auth code replay: Track used codes in oauth_used_codes table with
  unique constraint. Codes are single-use per OAuth 2.1 §4.1.2.
- Open redirect: Validate redirect_uri against hardcoded allowlist
  of known Claude callback URLs + localhost for dev.

P1 fixes:
- Move API key creation from /authorize to /token endpoint. Keys are
  only created after PKCE verification, preventing orphaned keys on
  abandoned OAuth flows.
- Add ensureInitialized() to MCP server so event handlers load and
  transaction.categorized events reach extensions.

P2 fixes:
- Remove 'plain' from PKCE methods — only S256 is advertised and
  accepted.
- Fix extension count in sectors test (10 → 11 for mcp-server).

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

* fix: remove duplicate ensureInitialized() that caused circular import

The extension router (ext/[...path]/route.ts) already calls
ensureInitialized() before dispatching to handlers. The duplicate
call in server.ts created a circular import that Turbopack couldn't
resolve, breaking the Vercel build.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-21 12:57:14 +01:00