* feat(auth): enforce session idle and absolute timeouts
Hosted browser sessions now carry an HMAC-signed, HttpOnly cookie holding
session start, last activity and sign-in method, bound to the Supabase
session. Middleware enforces a 30 min idle and 12 h absolute limit
(reason-coded redirects to /login), a heartbeat route advances idle
activity from real user input, and a client controller warns 2 minutes
before expiry. BankID users are routed back to BankID on re-auth via a
short-lived method hint. API-key and MCP bearer surfaces are exempt;
self-hosted installs default off and can opt in via env vars.
Fixes#362
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): derive session-timeout signing key via HKDF
The HMAC key is now HKDF-derived with a purpose-bound info string, so
the SUPABASE_SERVICE_ROLE_KEY fallback never uses the privileged
credential directly as a signing key. Addresses the security review
finding on PR #1387.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): back signature bytes with a plain ArrayBuffer
crypto.subtle.verify requires a BufferSource; Uint8Array.from is typed
over ArrayBufferLike, which the Vercel TypeScript build rejects. Decode
base64url into a Uint8Array constructed over a fresh ArrayBuffer.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(auth): address session-timeout review findings
- signSessionTimeoutState returns null on signing failure instead of
throwing, so a missing secret degrades the timeout feature in line
with verifySessionTimeoutState rather than crashing authenticated
requests; middleware and heartbeat skip the cookie write when null
- heartbeat initializes a fresh signed state for a missing or
session-mismatched cookie, mirroring middleware, instead of
returning SESSION_EXPIRED during normal initialization
- sessionStateMatchesUser treats an unresolved current session id as
a mismatch for session-bound state so another session's cookie is
never accepted on the userId fallback alone
- drop aria-live from the countdown DialogDescription so screen
readers are not interrupted every second
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat(onboarding): illustrated halftone backdrop from marketing-site art
Ports the gnubok-website halftone illustration set into the onboarding
flow so signup -> app feels like one product:
- OnboardingBackdrop: petal field across the paper background (radially
masked to stay calm behind the form), Stockholm skyline dissolving
into the bottom edge, two clouds drifting at reading pace that bounce
off the viewport and the onboarding panel (IllustrationFloaters,
physics ported from the website's BouncingFloaters).
- Per-step instrument ghosted in white ink on the dark card header:
pencil -> notebook -> adding machine -> calculator, one per step.
- Dark mode via invert/hue-rotate filters; prefers-reduced-motion parks
the floaters; all art is aria-hidden and pointer-events-none.
Applies to /onboarding, /onboarding/agent and /select-company via the
shared (onboarding) layout. No new strings, no API changes.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* refactor(onboarding): strip backdrop to skyline + step art per founder review
Founder kept the Stadshuset skyline and the per-step header instruments;
the petal field and drifting clouds are cut. Removes the now-unused
floater physics component and the petals/cloud assets.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
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>
* chore: remove Sentry, consolidate migrations, add test coverage
Remove @sentry/nextjs and all Sentry integration code — error tracking
now handled by Recapt. Consolidate 22 incremental migrations into a
single schema sync migration. Add 6 new test suites (auth, invoice
matching, VAT rules, opening balances) and extend report tests with
edge cases. Update Docker image name to gnubok, sync crontabs and
extension presets, fix CSP missing space, simplify journal entry
missing-document dialog.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove viewer bank import migration never applied to production
20260413150000_viewer_bank_import_permissions.sql (PR #234) was merged
to main but never applied to the production database. It references
current_active_company_id() which does not exist in production either.
This breaks fresh installs and Supabase preview branches because the
migration runs before the consolidated schema sync.
Remove it so the migration chain matches production. The viewer bank
import RLS policies should be re-added in a future migration alongside
the helper functions they depend on.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct delete policies for tables without company_id column
Seven tables in the generic delete-policy loop don't have a direct
company_id column, causing fresh installs to fail with "column
company_id does not exist". Fix by moving them out of the loop:
- invoice_items, journal_entry_lines, receipt_line_items,
supplier_invoice_items → join through parent table
- extension_toggles, notification_settings, push_subscriptions →
user-scoped (auth.uid() = user_id)
All policies match their existing production definitions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Implement company and account deletion features
- Add event types for company and account deletion to CoreEvent.
- Enhance Supabase middleware to handle company context resolution and cookie management for archived companies.
- Create API routes for deleting accounts and companies, including necessary validations and event emissions.
- Implement tests for account and company deletion endpoints to ensure proper functionality and error handling.
- Add retention notice component to inform users about bookkeeping data retention during destructive actions.
- Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws.
* feat: enhance account deletion process and update user notifications
* Add service client for onboarding completion check and update escape hatch visibility
* Enhance invite flow and email handling for company members
* Refactor company context and RLS policies for active company isolation
- Update `switchCompany` to remove unnecessary revalidation as client handles navigation.
- Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships.
- Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility.
- Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership.
- Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization.
- Implement `CompanyTabSync` component for real-time active company enforcement across tabs.
- Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`.
* feat: implement viewer role enforcement for write permissions
- Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company.
- Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions.
- Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers.
- Created tests to verify the behavior of the viewer role and write permissions.
- Added database migration to enforce read-only access for viewers at the database level.
* Refactor onboarding and dashboard logic; add silent team creation for users
- Removed unnecessary useCompany context in DashboardContent and SettingsSidebar components.
- Simplified onboarding setup logic to allow direct access to the dashboard for users without companies.
- Introduced WelcomeOnboarding component to handle user onboarding steps.
- Added migration to create silent teams for all users at signup, backfilling existing users without teams, and cleaning up incomplete companies.
* fix: update greeting logic and improve email handling in TIC extension
* Redirect to onboarding for users without companies and update onboarding flow
* Build issue fix
* Enhance onboarding experience by adding existing companies check