c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
32 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
6a85efb00a |
feat(mcp): allowlist Grok's connector callback and document the Grok path (#2158)
* feat(mcp): allowlist Grok's connector callback and document the Grok path Grok custom connectors self-register through /api/mcp-oauth/register with redirect_uri https://grok.com/connectors-oauth-exchange-code/, which the built-in allowlist rejected with invalid_redirect_uri before consent. Add the callback as an exact-path BUILT_IN_PATTERNS entry (trailing slash optional, no prefix) with provider 'grok', named "Grok (xAI)" on the consent page. Tests: accept, foreign-host and other-path rejection, provider mapping, and a register route test for the Grok DCR shape. Surface Grok next to ChatGPT: a "Using Grok?" side door on the onboarding Claude step (one side door open at a time, telemetry step grok), a Grok row under "Other clients" in the API & MCP settings tab using ?client=grok, and sv/en strings for both. Docs: mcp-server rule, ARCHITECTURE, README, registry entry (install section), DECISIONS. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa Signed-off-by: Emil <emilmattsson14@gmail.com> * fix(mcp): cite X Corp's published Grok callback, test the consent label Review pass on #2158: the allowlist comment and DECISIONS entry claimed xAI publishes no callback and the value came from a live observation; X Corp lists https://grok.com/connectors-oauth-exchange-code/ as the "Grok (web)" redirect URL at docs.x.com/x-ads-api/mcp, and grok.com serves the path itself (slash form 308s to no-slash on the same origin). Reworded both to cite that. Adds the consent-page test for "Grok (xAI)" next to the ChatGPT one and a JSDoc on the onboarding side-door toggle. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGbspj3hiNqvqTWZqdwysa Signed-off-by: Emil <emilmattsson14@gmail.com> --------- Signed-off-by: Emil <emilmattsson14@gmail.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
18cbc4c30a |
fix(security): audit remediation 2026-09-01: api_keys identity, viewer gates, OAuth binding, XSS, MFA gate (#2155)
* fix(security): bind api_keys to the caller, lock hash-as-bearer RPCs and provider token tables Security audit 2026-09-01, critical items. - api_keys INSERT requires user_id = auth.uid() again (an admin could forge a key for any co-member and act as them in every company they belong to); SELECT is own-keys-or-admin; a BEFORE trigger freezes the identity and credential columns against user-session UPDATEs. - rotate_mcp_refresh_token and validate_and_increment_api_key become service_role only: they match rows by a presented SHA-256, so a hash readable by co-members was a bearer credential. - validate_and_increment_api_key fails closed when the key's user is no longer a member of the key's company. - provider_consent_tokens and provider_otc: the DELETE policies collapsed to "caller has any team row" (correlated subquery on a non-existent team_members.company_id). All member policies dropped; service_role only, matching every existing code path. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): role gates, ownership guards and posting integrity in the database Security audit 2026-09-01, high items at the database layer. - One table-level guard, enforce_company_writer_role(), blocks the read-only viewer role on 55 company-scoped tables including through the 15 membership-only SECURITY DEFINER writers. Keyed on the JWT role claim so it fires inside definer bodies; no-op for service_role and trigger cascades. - company_members user_id/company_id immutable from user sessions; invitations can never grant owner; team_members gains a transition guard (admins keep non-owner role moves); companies team_id and archiving are owner-only and team attachment needs team membership. - Direct statements (current_user = authenticated) can no longer insert posted headers, add lines under posted verifikat, or post a draft with a voucher number the sequence never issued. Sanctioned RPCs run as the definer and are untouched; the engine's own draft-then-post shapes still pass. - create_document_version refuses viewers and foreign storage paths; validate_version_chain needs membership and loses anon EXECUTE; match_documents / match_booking_templates lose anon; cron maintenance RPCs become service_role only; the production-only seed_asset_categories is dropped. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * build: pin tsx as an exact devDependency instead of fetching it with npx at build time prebuild ran "npx tsx" with no lockfile entry, so every Vercel, Docker and CI build downloaded tsx@latest and its transitive tree from the registry with no integrity check, inside the build environment. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): refuse the viewer role on API-key and MCP write paths The v1 wrapper and the MCP company routing checked company membership but never role, and both run as service role, so a read-only viewer holding an API key could post vouchers and change settings through the API. Mutating methods and non-read scopes now return 403 ROLE_READ_ONLY for viewers on v1; MCP write tools refuse viewers the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): stop serving uploaded SVG, XML and HTML as executable content on the app origin Uploads persisted the browser-declared mime type and the inline proxy served it verbatim, sandboxing only text/html; the storage proxy forwarded the uploader's Content-Type. Any writer, or any Peppol sender, could plant a scripted SVG or XHTML that executed on app.gnubok.se. - inline route: allow-list of natively safe types (PDF, raster images) served as before; everything else gets the opaque sandbox CSP. - storage proxy: octet-stream + attachment + sandbox unless the DB mime for the key is on the allow-list. - document-service: the stored mime is the magic-byte validated type. - logo upload: magic-byte validation, SVG refused. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): byrå brand logo upload decides the type by magic bytes and drops SVG Same pattern as the company logo route: the logos bucket is public, so a scripted SVG (or anything declared as an image) must never land there. The upload pickers stop advertising SVG. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): bind Enable Banking, Stripe and WooCommerce callbacks to the initiating user The callbacks resolved the pending row by oauth_state alone, so a victim who completed an attacker-initiated consent had their bank account, merchant account or store attached to the attacker's company. requireFlowInitiator() now requires the cookie session of the user who started the flow: no session redirects to login with the callback URL preserved, a different user is refused and nothing is exchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): guard tenant-controlled outbound fetches and surface the disabled rate limiter WooCommerce and Shopify syncs fetched a member-editable store URL with plain fetch() and redirect following under the service role, and the invoice PDF renderer fetched company_settings.logo_url unguarded. All three go through a new safeFetch() (public-IP validation via url-guard, https only, redirect: 'manual', body size cap) and re-normalise the stored host at use time. checkRateLimit() keeps failing open on hosted but logs one error per process when Upstash is not configured and exports isRateLimiterConfigured(). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): decide the API MFA gate from server-authenticated factors, not the session cookie getAuthenticatorAssuranceLevel() without arguments derives nextLevel from session.user.factors, which comes from the unsigned sb-*-auth-token cookie. Deleting factors from the cookie made an enrolled account look like it had nothing to step up to, on every /api route and in requireAuth. Both gates now read factors from the getUser() result or listFactors() and the level from the verified JWT claim, and fail closed on errors. Page-branch gate hardened the same way. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): bind Fortnox/Visma, Gmail and Skatteverket callbacks to the initiating user The arcim-migration callback exchanged the provider code onto whatever consent the one-time state named, with no check of who completed the flow and no org-number comparison, so a phished Fortnox admin handed their ledger to the attacker's company. provider_otc now records the initiating user (migration 20260902100000); the callback requires that session and, after the exchange, refuses a provider company whose org number differs from the consent's company. The Gmail and Skatteverket callbacks enforce the same initiator check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): BankID signup confirms the email before linking the identity Signup created an email-confirmed, MFA-exempt account for any address the caller typed and returned a magic link, so an attacker could pre-register a victim's email and keep a permanent BankID login into the account the victim later adopted. The user is now created unconfirmed, the identity carries email_verified_at NULL (migration 20260902101000), bankid_linked is not set until the mailed confirmation is clicked, and BankID login of a pending identity is refused with the confirmation re-sent. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(security): bind MCP OAuth redirect URIs to the consenting user and cap scopes A user-registered redirect URI was allowlisted globally, the consent page named no client, and all scopes were pre-checked, so one phishing link handed an attacker a full-scope key for the victim's company. Registered URIs now resolve only for the registrant or a colleague sharing a company; the consent page shows the client identity and redirect host; non-built-in clients default to read-only pre-checks; scopes are capped by the user's role (viewer: read only) at consent and at /token. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(auth): client follow-ups for BankID confirmation, callback mismatch copy and decision log - register client handles the new confirmation_sent response from BankID signup with the existing inbox screen instead of calling verifyOtp. - BankID login surfaces the email_unconfirmed explanation. - WooCommerce settings map woocommerce_error=wrong_user to its own copy. - Logo help text no longer advertises SVG. - DECISIONS.md records the audit remediation choices. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * fix(mcp-oauth): literal SoD columns in the api_keys insert so the phantom-column scanner resolves them Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(logo): type the upload fixtures as Uint8Array<ArrayBuffer> so they are valid BlobParts Fixes the typecheck ratchet on PR #2155 and ratchets the baseline down by the one legacy error the change removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
9fe37b85b5 |
feat(agents): per-key approval authority, the amount an agent may post unattended (#2079)
* feat(agents): per-key approval authority, the amount an agent may post unattended An API key gets an optional ceiling in SEK. Above it the agent may still stage the work, it just may not finish it alone: a human approves the same verifikat in the app. Default is NULL, so every existing key keeps its behaviour and turning this on is entirely opt-in. Enforced at the two places an API key reaches the ledger, and at both the refusal happens BEFORE the point of no return: - MCP: in commitPendingOperation, before the atomic claim, so the operation stays 'pending'. Behind the claim it would be caught by the generic handler, marked terminal 'rejected', and the staged verifikat would be gone. - REST: in journal-entries.commit, before commitEntry, so the draft stays a draft and the voucher sequence never advances (BFL 5 kap. 7 §). The dry run refuses too, rather than promising a voucher number the key cannot deliver. Not enforced inside commit_journal_entry: a RAISE there is swallowed by engine.ts into a retryable 500, and it would cost a DROP+CREATE on the function that issues every voucher number. Operations whose amount is only known during dispatch (batch allocation, bulk booking, the settlement link paths) fail OPEN behind an explicit allowlist. Pricing them ahead of dispatch would be a guess, and a wrong guess silently breaks batch allocation the day someone sets a limit. The allowlist is derived from what production actually stores: create_voucher carries total_debit on 1389 of 1389 rows, categorize_transaction carries amount on 2002 of 2003, create_supplier_invoice_from_inbox carries total on 208 of 228. This is a blast-radius cap, not a security boundary. A per-entry ceiling is defeated by splitting one entry into several, and an LLM will find that, so UNATTENDED_COMMIT_LIMIT_EXCEEDED forbids splitting first: one affärshändelse is one verifikat (BFL 5 kap. 6 §). A cumulative rolling-window limit is the primitive that actually bounds exposure and is left to a separate change. The guard is written NULL-first everywhere. An absent, unparseable or non-positive ceiling always means unlimited, never "block everything". Agents read their own ceiling from gnubok_get_agent_briefing instead of discovering it by burning a staged verifikat on a 403. Changing a ceiling is auditable: it now renders in behandlingshistorik (BFL 5 kap. 11 §). The audit trigger already fired on the column, but the report dropped the event because the field was not in its diff map. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(skill): regenerate accounted-api skill for the new commit pitfall apiskill:check is a ratchet: the generated reference must match the endpoint registry. Never hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * test(agents): pin the DB default itself, and declare the briefing field required Two review findings, both real: - the default test stored an explicit NULL, so it stayed green even if the column default changed to a positive ceiling: the one change that would silently start blocking every existing key. It now omits the column. - gnubok_get_agent_briefing documents unattended_commit_limit as always present and emits it unconditionally, so it belongs in the output schema's required list. Declined the NOT VALID constraint suggestion, with the reason recorded in the migration: api_keys is 388 rows / 768 kB in production. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(agents): name the TOCTOU window in the REST ceiling check A security scan flagged that the line sum is read before commitEntry, so a concurrent write to the draft's lines can post over the ceiling. Real, and accepted: closing it means enforcing inside commit_journal_entry, where a RAISE becomes a retryable 500 and destroys the staged operation on the MCP path. Recorded in the code rather than left implicit, so nobody later mistakes this for a hard control. A per-entry ceiling is already defeated by splitting, which needs no race; the cumulative rolling-window limit is the primitive that bounds exposure. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(agents): price the settlement and batch paths that were bypassing the ceiling A security scan flagged that known money-posting operations fail open, and it was right. The first cut priced only create_voucher, categorize_transaction and create_supplier_invoice_from_inbox, on the belief that the batch and settlement paths computed their totals only inside SQL at dispatch. Production says otherwise: the staged preview already carries the amount, because it is the number a human is shown when approving the operation. Over the last 120 days each of these is present and numeric on 100% of that type's staged rows: link_transaction_journal_entry transaction_amount 1369 rows bulk_book_transactions tx_sum 273 rows link_supplier_invoice_voucher payment_amount 55 rows match_batch_allocate total_allocated 24 rows mark_invoice_paid total 3 rows So a key with a ceiling could post any amount through the four largest settlement paths. Now priced, and the ceiling applies. Only reconciliation_match stays unpriced: it carries pair_count, which is a COUNT. Pricing off that would compare pairs against kronor, which is worse than not enforcing. link_document_to_voucher and attach_document_to_transaction move no money at all; the transaction_amount they carry is context, not a posting. Genuinely unpriceable types still fail OPEN. This control can only ever narrow what a key does, and a wrong guess at an amount blocks a legitimate commit, so guessing high would leave an agent unable to work. Adds a test that walks the whole allowlist, so a typo'd field name cannot silently make a type unpriceable again: that is exactly the hole this closes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mcp): drop the ceiling from the agent briefing, the payload budget has no room The tools/list context-budget bench sits at 65 000 tokens and main now leaves roughly 20 tokens of headroom. An always-present field on the briefing's output schema costs about 85, so this addition alone pushed the bench red. The bench's own note is explicit that the answer is to demote a tool rather than raise the ceiling, so raising it here would be the wrong trade for a nice-to-have. Nothing is lost that matters: the operation is never destroyed when it is refused, so discovering the ceiling from UNATTENDED_COMMIT_LIMIT_EXCEEDED costs one round trip and no work. That error already carries both attempted and limit, and GET /api/settings/api-keys returns the value. Re-exposing it on the briefing is worth doing once there is budget to spend. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(api): spell affärshändelse correctly in the commit pitfall Fixed in the route's registerEndpoint pitfalls, which is the source; the skill reference is regenerated from it and never hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6ac9679fb5 |
feat(auth): base available login methods off GoTrue providers (#1869)
* feat(auth): base login fields on GoTrue providers Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # app/(auth)/login/login-client.tsx # app/(auth)/register/page.tsx * fix: address feedback Signed-off-by: Goostaf <gasplund2@gmail.com> * chore: remove hardcoded Google enabled checks Signed-off-by: Goostaf <gasplund2@gmail.com> # Conflicts: # .env.example * feat: show label when password login is disabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: use MicrosoftMark, correct comment Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom providers Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: display custom provider labels Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: add SAML login path Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: show when no methods are available Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: display SAML button when enabled Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: preserve nextPath and broken key Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: redirect test to client Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: expose registerEnabled Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: provider allowlist and request timeout Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: restore compact labels Signed-off-by: Goostaf <gasplund2@gmail.com> * refactor: move withTimeout implementation to utils Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: include nextPath Signed-off-by: Goostaf <gasplund2@gmail.com> * fix: export function and test case Signed-off-by: Goostaf <gasplund2@gmail.com> * feat: only show SAML button if vars configured Signed-off-by: Goostaf <gasplund2@gmail.com> * fix(auth): map SAML sign-in error through getErrorMessage The antipattern ratchet (check:guards, raw-user-error) rejects a raw error.message reaching a user-visible sink. Route the signInWithSSO error through getErrorMessage like the other auth error paths. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * feat(auth): GitHub brand mark on the provider button; decision log GitHub allows its invertocat in solid black/white, so currentColor is correct; custom OIDC providers keep the generic key icon. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013BAzJjXQBa9F5L1U42wUMj Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> --------- Signed-off-by: Goostaf <gasplund2@gmail.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4f6ecad549 |
feat(white-label): invite-only signup for brand domains (#1995)
* feat(white-label): invite-only signup for brand domains
A brand domain belongs to the partner's people (founder decision
2026-08-27): only allowlisted or invited users may create an account on
an invite-only brand domain; everyone else is shown an interstitial that
sends them to the canonical Accounted signup.
- brands.signup_mode ('open' default / 'invite_only') +
brand_signup_allowlist (lowercase emails, team-scoped RLS, owner/admin
writes) + create_company_for_brand_signup RPC, with pg-real coverage
- server-side gate (lib/auth/brand-signup-gate.ts) enforced on every
signup path: email signup moved to POST /api/auth/signup (the browser
used to call GoTrue directly, so a client-side check would be
bypassable), BankID gated in /bankid/complete, Google covered by the
dashboard layout's brand-domain bounce
- company invites bypass the allowlist: the invite is the authorization
- register page interstitial on gated brands (no email in the outbound
URL), sv+en strings
- dashboard layout bounces non-belonging sessions off gated brand hosts
to the canonical domain (navigation rule like WL-01, not a security
boundary)
- allowlisted signups' onboarding-created companies attach to the
brand's byra team via the new RPC, so WL-01 homes them on the brand
domain; the allowlist entry recorded by an owner/admin stands in for
the WL-15 admin gate
- byra cockpit page /clients/access + /api/clients/signup-access to
manage the mode and the allowlist
All existing brands default to 'open': behavior is byte-identical until
a brand is flipped to invite_only.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByL5dQXG8gGLtNBPj8g2C4
* fix(white-label): rollback brand-signup company with the service client
Skeptic (correctness) found that a brand-signup company created under the
service role rolled back with the cookie-session client: `companies` has
RLS and no FOR DELETE policy, so the delete was a silent 0-row no-op,
stranding a member-less ghost company on the partner's byra team. Pass an
optional rollbackClient to createCompanyCore and hand it the service
client on that path; user_preferences.active_company_id then clears itself
via its ON DELETE SET NULL FK once the company row is actually deleted.
Also map a validateBody 400 (flat envelope, no code) on the register page
to the specific email-invalid field message instead of the generic one,
since the client already pre-gates password strength.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByL5dQXG8gGLtNBPj8g2C4
* fix(white-label): fail-safe brand lookup, pg-test seed, anonymize fixtures
Second resolve-pr cycle: skeptic + CodeRabbit findings and a green-up.
- Fail safe on a brands-table error (CodeRabbit CWE-285): the gate treated a
failed resolveBrandByHost as an unbranded host, opening invite-only signup
during a transient DB blip. resolveBrandResultByHost now distinguishes
"no brand" from "lookup failed"; the gate returns lookupFailed and the
email + BankID routes answer 503 (retry), never creating an account.
- pg-real: the RLS delete test seeded its row inside withUserContext, which
always rolls back, so the owner DELETE saw zero rows. Seed on the superuser
pool instead.
- Anonymize every test/fixture brand to the repo's existing synthetic
placeholder (Siffra / app.siffra.se): no real partner names in code.
- SignupAccessManager: functional setData updates so a concurrent mode
toggle and an add/remove do not clobber each other's snapshot (CodeRabbit).
- Route a transient-error message through i18n instead of the raw envelope
(raw-user-error guard); new register.error_temporary sv+en.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByL5dQXG8gGLtNBPj8g2C4
* test(white-label): anonymize new signup-gate fixtures; log oracle residual
Rename the placeholder brand in the four new brand-signup test files to a
clearly-fake, partner-unrelated name (Testbrand / app.testbrand.example);
the previous placeholder echoed a real partner. Scoped to files this PR
creates; the repo-wide legacy placeholder is left for a separate cleanup.
Also record in DECISIONS.md that the feature ships accepting the
low-severity allowlist-enumeration residual (captcha-free 403 vs 200 on
the signup endpoint), with rate-limiting as the follow-up option.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByL5dQXG8gGLtNBPj8g2C4
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
c31933b15b |
perf(api): write routes stop re-resolving the active company (#1928)
* perf(api): write routes stop re-resolving the active company
withRouteContext resolves the active company (one resolve_active_company
RPC, ~40 ms p50 on prod) and then, for the 256 routes that pass
requireWrite: true, called requireWritePermission(), which resolved it a
second time before its role select. Two sequential round trips repeating
work the wrapper had just done, on every mutating request.
requireWritePermission() and getCompanyRole() now accept an optional
`known` context; the wrapper passes { companyId }, so the helper goes
straight to the membership select. Callers that pass nothing behave
exactly as before, and the shared selectRole() keeps both helpers on the
same query. The role is still looked up, never trusted from the caller.
Tests: known companyId skips resolution, known role skips the select, a
known viewer is still 403, a known company without a membership row is
still 403, legacy calls unchanged; new lib/api/__tests__/with-route-
context.test.ts pins that the wrapper resolves the company exactly once,
hands it to the guard, never calls the guard on read routes, passes the
guard's 403 through with a request id, and emits Server-Timing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(customers): viewer gate expects the wrapper to hand over the resolved company
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
b8605aabfc |
fix(settings): derive API-key scope groups and tool counts from the scope catalogue (#1924)
The API-key settings panel carried a hand-copied list of scope groups that
had drifted to 24 of the 30 scopes in API_KEY_SCOPES: articles:read/write,
companies:write and the three reconciliation scopes were missing, so a key
minted in the dashboard could not call gnubok_create_company, the article
tools, the seven reconciliation tools or the matching v1 endpoints. The
per-scope "N verktyg" counts in the panel and in the API_KEY_SCOPES
descriptions were hand-maintained and wrong (reports:read said 18, actual
30; bookkeeping:write said 11, actual 22).
- Move the pure scope catalogue (API_KEY_SCOPES, scope lists, SCOPE_GROUPS,
TOOL_SCOPE_MAP) into lib/auth/scope-catalog.ts with no server imports, so
the client-side panel can bundle it. api-keys.ts re-exports everything,
so existing imports are unchanged.
- SCOPE_GROUPS becomes a list of { domain, label, scopes } covering every
scope (reconciliation has three), shared by the panel and the OAuth
consent page. scopeKind() replaces the ad hoc suffix checks.
- TOOL_COUNT_BY_SCOPE is derived from TOOL_SCOPE_MAP at module load; the
hand-written counts are removed from the catalogue descriptions.
- The panel renders groups and cards from the catalogue; i18n keys are
derived from domain and scope id. The "(REST API)" heading suffix is
computed from the counts instead of baked into the labels.
- New unit test asserts every scope belongs to exactly one group and that
counts equal TOOL_SCOPE_MAP occurrences.
- New sv/en strings for the articles, companies:write and reconciliation
scopes.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
d3869e6694 |
fix(api): register the v1 stamp endpoint scope and derive the webhook event catalogue from one source (#1930)
POST /api/v1/companies/{companyId}/inbox-items/{id}/stamp registered itself
with scope documents:write but had no V1_ENDPOINT_SCOPES entry, and the
wrapper resolves the required scope from that map before it validates the
bearer token, so the route answered NOT_FOUND to every caller. Add the entry,
drop the three phantom entries that had no route (GET openapi.yaml, GET
companies/:companyId, GET companies/:companyId/events), and add a parity test
that pins the scope map to the endpoint registry in both directions, checks
every pattern against an existing route file, and checks every v1 route file
is imported by load-routes.ts.
The webhook event catalogue was hand-copied in three places and had drifted:
the fan-out handler delivered 28 events while the v1 create enum, the OpenAPI
spec, the generated agent skill and the docs page listed 24, so the four
reconciliation.* events could not be subscribed to. lib/webhooks/public-events.ts
is now the single source; the handler set, the Zod enum and the docs section
derive from it, with tests that pin each surface to the catalogue. The PATCH
webhook docs no longer tell agents to delete and recreate a webhook to rotate
its secret: POST .../rotate-secret exists.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
328ccda10d |
fix: protect public Auth flows from automated abuse (#1904)
* fix: add Turnstile to public auth flows * test: isolate Turnstile auth tests |
||
|
|
a717f03898 |
feat(mcp-oauth): let an account with no company connect and sign up from the OAuth popup (#1814 PR 1) (#1855)
* feat(mcp-oauth): let an account with no company connect and sign up from the OAuth popup Identity unlock for agent-first onboarding (#1814, shape B+). A person with no Accounted account can now connect from an MCP client, create the account inside the Connect popup and finish the OAuth dance. - authorize/token no longer require a company: consent renders a companyless variant and the key is minted with company_id NULL. - validateApiKey returns companyId string|null and binds an unbound key to the user's first company on the first validation after it exists. - MCP server: company-dependent tools and data resources answer with a structured NO_COMPANY_YET error; the company-independent tools still run; telemetry skips when there is no company scope. - /api/events fails closed instead of throwing for an unbound key. - authorize forces TOTP enrollment (not just verification) for password accounts with no factor, since the middleware skips enrollment for zero-company users; BankID-linked accounts stay exempt. - /login forwards next to /register; register, GoogleAuthButton and /auth/callback carry it back to the consent page (callback honours only /api/mcp-oauth/authorize, via safeReturnTo); /mfa/enroll hard-navigates to /api/* destinations like /mfa/verify. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * refactor(company): move getActiveCompanyId out of the next/headers module lib/auth/api-keys.ts needs the resolver for unbound-key binding, but lib/company/context.ts imports next/headers for the legacy company cookie and Turbopack refuses that import on some of api-keys' import paths (the preview build failed). The resolver and CompanyContextError now live in lib/company/active-company.ts; context.ts re-exports them so every caller and test mock is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 * fix(mcp-oauth): fail closed on a failed assurance lookup; enroll Back aborts instead of looping Review findings on #1855: requireAal2 let consent through at AAL1 when getAuthenticatorAssuranceLevel() returned nothing and a verified factor existed. Only a positive AAL2 answer passes now; a failed lookup and the inconsistent verified-factor-at-AAL1 case both step up to /mfa/verify. Back on /mfa/enroll with the consent page as returnTo went straight back into the redirect loop; it now aborts to the app. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018wCdzRTatKiDByKB8hCNT6 --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
38a890c8d1 |
fix(underlag): carry the phone photo that is too big to send, and say why when we cannot (#1550)
* fix(whatsapp-inbox): register the channel question event types Every follow-up question the WhatsApp intake asks has been failing its processing_history append in production: ChannelQuestionAsked, ChannelQuestionAnswered and ChannelQuestionExpired were never added to the processing_event_types catalog the event_type FK points at. appendQuestionHistory() catches and logs that failure by design, so the reply to the sender still goes out and nothing looked broken from the outside. What was lost is the durable record of the exchange, which is part of how the underlag was obtained (BFNAR 2013:2 kap 8). Catalog rows only: aggregate_type 'System' already passes the CHECK. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(underlag): say why an upload failed, and get out of an expired session A user reported that none of the three ways to add a receipt from a phone worked, all of them answering "Uppladdning misslyckades. Nagot gick fel, forsok igen" immediately. Production told us nothing: every upload request that reached the route in the same 24 hours returned 200. Both halves of that are the same bug. The workspace read failures as `throw new Error(json.error)`, which loses a body that is not JSON (the res.json() call throws first) and stringifies the structured envelope to "[object Object]", so anything the route did not answer with a plain string arrived as the generic fallback. The middleware 401 for an expired cookie session is exactly that envelope shape, and a phone tab left open is exactly where the session expires unnoticed: the controller's timers are throttled in the background, so the request the user just made is what finds out. Now the response is resolved where it fails, through the house helper that already knows the status map, and an expired session is announced on the session-timeout BroadcastChannel so the controller signs out and routes to /login the same way it does for an expired heartbeat. Failed uploads also post metadata (status, size, mime type, resolved reason) to /api/log, the one API path exempt from the timeout gate, so a request answered before the route runs stops being invisible. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(underlag): carry the phone photo that is too big to send The reported failure was not the account and not the session: hosted rejects any request body over 4.5 MB itself, before the function runs. Measured against production, 4.4 MB reaches the route and 4.6 MB comes back as a plain-text FUNCTION_PAYLOAD_TOO_LARGE. Nothing invokes the function, so nothing lands in the logs, which is why one user's failing uploads were invisible while every upload that arrived returned 200. An iPhone photo in "Most Compatible" mode is 4-12 MB, so whether it worked depended on whose phone took the picture. Meanwhile the route advertises a 10 MB limit it can never be handed. Photos are now re-encoded in the browser when they exceed what the platform will carry: 2400px on the long edge at JPEG q0.85, stepping the quality down only if that is not enough. That keeps the small print on a receipt legible, which is what BFL 7 kap asks of an archived underlag ("varaktigt läsbart skick", a faithful reproduction), and a refusal is not. What cannot be shrunk (a PDF, or HEIC where the browser will not decode it) is refused before the upload starts, naming its actual size and the limit rather than failing in transit. 413 joins the HTTP status map so a rejection we cannot pre-empt still says what happened: the platform's body is plain text, so the status is the only thing there is to translate. Self-hosted Docker has no proxy in front of the app, so none of this applies there and the route's own MAX_FILE_SIZE keeps governing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
11995b1b0c |
feat(auth): make automatic logout an opt-in per-user setting (#1536)
* feat(auth): make automatic logout an opt-in per-user setting Session timeouts (30 min idle / 12 h absolute on hosted) now apply only to users who enable "Automatic logout" in Settings > Security. Default is off: sessions live for the full Supabase refresh-token lifetime, the behavior from before the 2026-07 session hardening. - user_preferences.auto_logout (migration, default false), toggled via the extended /api/user/preferences route - The opt-in is snapshotted into the signed timeout cookie at mint, so enforcement stays DB-read-free per request; the preferences route clears the cookie on change so a toggle takes effect immediately - Pre-toggle cookies are authentic-but-stale: re-minted preserving their timers, never routed down the tamper path, so the rollout does not log anyone out - NEXT_PUBLIC_SESSION_TIMEOUT_FORCE_ALL=true enforces timeouts for every user regardless of preference (emergency lever, also plumbed through the Docker image); self-hosted stays disabled by default Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): resolve PR #1536 review findings - Replace the spread upsert in /api/user/preferences with one literal payload per field: the phantom-column schema guard cannot resolve spread payloads (Unit tests 3/4 ceiling failure) - Map the preferences 500 through getErrorMessage so the user-facing text is Swedish (CodeRabbit) - fetchAutoLogoutPreference now returns null on a FAILED read instead of a fail-open false: callers skip minting so an unknown preference is never persisted into the year-long signed cookie, and the next request retries; failures log at error level, distinct from the normal opt-out path (compliance swarm GDPR Art.32(1)(b) / ISO A.8.5) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): write multi-field preference updates as one atomic upsert A request carrying both hide_assistant_fab and auto_logout previously issued two sequential writes, so a failure of the second returned 500 after half the request had persisted (CodeRabbit, PR #1536). One literal upsert per accepted field combination keeps the write atomic and stays resolvable for the phantom-column schema guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
645ed0a53e |
feat(login): method-state login panel with quiet inline errors (#1469)
* feat(login): method-state login panel with quiet inline errors The login panel now shows one method at a time (the pattern Swedish users know from banks, Kivra and Fortnox): BankID as the hero state, the email form as a peer state, and the remaining methods as two quiet half-width chips under a single divider. The last successful method is remembered in an accounted-login-method cookie, read server-side so a returning password user gets the form on the first paint with no flash. Error display drops the boxed banner everywhere: credential failures render as one destructive sentence directly under the password field (fields keep aria-invalid), and the reset-password action surfaces from the second consecutive failure. BankID/Google failures, callback errors and the session-timeout notice are single quiet lines at the top of the panel (AttnLine for the informational one). Also: password visibility toggle, webkit autofill repaint to the theme surface, auth pages move from the gradient background to the app frame tone, register/MFA/reset get the same backdrop for cross-page coherence, and Skapa konto moves out of the panel into a footer line. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(register): mirror the method-state panel on signup Same treatment as the login page: BankID signup as the hero state, the email form as a peer state (live password checklist kept), alternatives as half-width chips under one divider, quiet-line notices instead of the blue box, subtitle dropped, footer harmonized. Successful signup persists the method hint so the user's first login opens correctly. The BankID-verified email-collection step keeps its panel takeover. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0bb0b89353 |
feat(auth): inline, specific error states on login and signup (#1440)
* feat(auth): inline, specific error states on login and signup Auth failures now render inline next to the form instead of as a top-right toast: a persistent alert with role=alert, aria-invalid field highlighting, and focus returned to the offending field. Login maps GoTrue error codes (invalid_credentials, email_not_confirmed, rate limits, user_banned) to specific Swedish/English messages, with a reset-password link embedded in the credentials error. The credentials message stays 'wrong email or password' by design: GoTrue returns one code for both cases to prevent account enumeration. Signup gets a live password-requirements checklist, field-level errors for weak/mismatched passwords, and inline handling of email-exists, invalid-email and rate-limit responses with a sign-in link where that is the recovery path. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(auth): treat email_provider_disabled as signup-disabled with specific copy Review follow-up: GoTrue signals disabled email/password signups with email_provider_disabled as well as signup_disabled; classify both (plus the message-string fallback for older GoTrue) and give the register form a specific inline message instead of the generic fallback. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
1c9d378df8 |
feat(auth): enforce session idle and absolute timeouts (#1387)
* 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> |
||
|
|
ff205951b1 |
fix(auth): store BankID personnummer ciphertext as raw bytea, not JSON-serialized Buffer (#1233)
Both writers of bankid_identities.personal_number_enc passed a raw Buffer
to supabase-js, which PostgREST serializes as JSON: every row stored the
literal text {"type":"Buffer","data":[...]} instead of iv|tag|ciphertext
bytes, so decryptPersonalNumber could never have read them (issue #1232).
- encryptPersonalNumberForStorage(): hex-encode for PostgREST bytea input
- decryptStoredPersonalNumber(): tolerant decode (raw bytea read-back,
legacy JSON-Buffer text, Buffer, serialized object)
- migration 20260727170000 rewrites existing rows to raw bytes; prefix
guard keeps it idempotent and skips already-raw rows. Conversion SQL
verified read-only against prod: converted bytes decrypt with the live
key (GCM tag valid, 12-digit result).
Closes #1232
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f24b26a139 |
fix: similar-sweep currency remediation, security hardening and v1 API fixes (#1215)
* fix(security): gate replace_sie_import behind owner/admin membership The RPC was SECURITY DEFINER with EXECUTE granted to PUBLIC and anon, no company_members lookup, no auth.uid() reference and no unauthorized raise, while setting gnubok.allow_delete to disarm the BFL immutability and retention triggers. Any caller holding a company_id and an import id could hard delete another tenant's verifikationer. Confirmed live in production. Applies the same fail closed owner/admin guard that undo_sie_import already carries (migration 20260624120000), resolving the actor from COALESCE(p_user_id, auth.uid()) so it denies when the role is NULL, then revokes EXECUTE from PUBLIC and anon. search_path and the raised statement_timeout are restated, since CREATE OR REPLACE drops settings that are not repeated. userId is a required parameter on replaceSIEImport: the service client has a NULL auth.uid(), so a caller without an explicit actor now fails to compile rather than hitting the closed gate at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): validate arcim OAuth callback state server side The callback route is skipAuth and decoded the state parameter as plain base64url JSON, trusting consentId and provider from it. A one time code was minted at flow start and never read. An unauthenticated attacker who learned a consent id could run an OAuth flow on their own provider account and post the callback with a forged state, landing their tokens on another tenant's consent, so the victim's next migration imported the attacker's ledger. State is now an opaque randomBytes(32) pointer to a provider_otc row, consumed by a single atomic UPDATE guarded on used_at IS NULL and expires_at, so a replay loses the row lock race and updates nothing. provider is read from provider_consents rather than trusted from the client. provider_otc already existed for exactly this purpose and was never wired up. Also scopes getConsent to an owning company, closing a cross tenant status oracle where the preview and migrate paths echoed a consent's status before the scoped check ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): scope documents storage to company_id (phase A) The documents bucket policies matched on auth.uid(), and upload keys were documents/{userId}/..., so company membership was never consulted. Removing a member revoked nothing: their session still authenticated and they kept direct Storage read access to every receipt, supplier invoice and bank statement they had uploaded. The same bug was fixed for sie-files in 20260416120000; this bucket was left behind. Phase A is additive. Company scoped policies are added alongside the uploader scoped ones, uploads move to documents/{companyId}/{userId}/..., and reads accept either layout so nothing breaks mid migration. Phase C, which drops the old policies, is gated on the backfill reporting zero remaining legacy prefix objects. The policy compares the company segment as text rather than casting to uuid the way sie-files does: this bucket holds keys whose second segment is not a uuid (MCP audit packages), and Postgres does not guarantee the bucket prefix qual runs before the cast, so a planner reordering would raise 22P02 and fail the whole query instead of filtering the row out. deleteDocument now removes both candidate keys. Removing only the stored pointer would leave a readable orphan copy of a document the user asked to erase. The backfill script is included but has never been run. It defaults to dry run, refuses .env.local by name, and verifies each copy is readable and SHA-256 identical before repointing the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): enforce events:read scope and membership on /api/events This was the only one of the three validateApiKey call sites with no downstream guard: v1 and the MCP server both check scope and re-verify company membership, this route did neither. An events:read scope existed and was documented as gating the endpoint but was never called, so a legacy key falling back to DEFAULT_SCOPES read the full log. The bound company id went straight from the api_keys row into a service role query, so a key whose user had been removed from the company kept reading. Adds the scope check before any database access, re-verifies company_members with archived_at IS NULL, honours test mode by stamping X-Gnubok-Mode instead of ignoring it, applies minimisePayload so the pull surface can never return a wider payload than the push surface, and replaces the three flat error strings with the canonical envelope. Test key reads are served rather than blocked: TEST_KEY_WRITE_BLOCKED is gated on mutations in with-api-v1, so a read gets the same treatment as every other v1 read endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(bookkeeping): sweep remaining journal_entries!inner embeds A previous refactor removed this pattern from lib/reports and introduced fetchEntryLines, but the class was never swept. Seventeen sites remained and had become the top application consumer of production database time: measured across the resulting query shapes, 32,694 calls and 25,848 seconds of execution, mean 790ms, with shapes averaging 2.6s and 3.0s and maxing at 7,962ms against the 8s statement_timeout, which surfaced to users as 500s on the booking path. PostgREST compiles an embed with filters on the embedded side into a correlated INNER JOIN LATERAL with a parameterized LIMIT, which stops Postgres reordering the join, so each query walked the whole journal_entry_lines table across all tenants. Driving from the entries side instead turns that into two indexed round trips. Converted sites keep their existing shape: the helper reattaches the parent entry under the same key the embed produced. Several conversions also remove a latent silent truncation where an unpaginated query was capped at PostgREST's 1000 row ceiling. Two deliberate exceptions. The free text ilike legs of the MCP display query stay on the embed, because each is capped at legLimit and that cap drives the truncation contract the tool reports, while the helper is unbounded. The accounts route moves to the existing get_account_usage_counts RPC instead, since its embed was a head count and the helper returns rows. commitEntry's write path is untouched: the change there is confined to the read query of the pre-commit dimension rule check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): anchor v1 list cursors on created_at Page two returned page one, forever, while still advertising a fresh next_cursor. The three routes sorted by and encoded a Postgres date column, which serializes as YYYY-MM-DD, but decodeDefaultCursor validates the cursor timestamp as full ISO-8601 and returned null, so the keyset filter was never applied and has_more never went false. An integrator syncing verifikat looped on the newest rows indefinitely. The transactions route already solved this and its comment names the trap; the fix was never ported. All three now order and encode on created_at with an id tie break, matching the transactions keyset predicate exactly. ISO_TIMESTAMP is deliberately left alone: relaxing it would silently change sort semantics on the route that currently works. Default ordering therefore moves from business date to insert order. Every business date is still on the row, and the invoices list gains date_from and date_to filters so a date range is still reachable; the other two already had them. The tests use an in-memory PostgREST that actually evaluates the filters, because the repo's pass-through mock cannot catch this class of bug: the bug is that the filter is never sent. They walk to exhaustion with a hard iteration cap, so an unterminated walk fails instead of hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): separate dry run from commit in the idempotency hash The request hash was built from url.pathname, which excludes the query string, so a dry run and its commit hashed identically. Following the flow documented in dry-run.ts, re-issuing the request with the same Idempotency-Key returned the cached preview with Idempotent-Replayed set and wrote nothing, while reporting 200. An agent or integrator saw success for a write that never happened. dry_run is folded into the hash only when true, not as an unconditional boolean. Including it as false would change the hash of every ordinary write, and with a 24h idempotency TTL any key in flight across the deploy would fail the request_hash comparison and 409 on a legitimate retry. Both hash call sites now go through one shared helper so they cannot drift into a permanent cache miss, and dry run responses are no longer stored at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: install the Bedrock SDK out of tree in the compliance review The Swedish accounting compliance gate had failed ten consecutive runs and so was posting nothing. With --no-package-lock npm discarded the lockfile and re-resolved the whole tree from package.json, floating @hookform/resolvers to 5.4.3, whose valibot ^1 peer conflicts with the pinned valibot 0.39.0. Installing into the parent of the checkout resolves only that one package, so an unrelated peer conflict can never take the gate down again. Node still finds it because ESM bare specifiers walk up parent node_modules; NODE_PATH would not have worked, as it is CommonJS only. --legacy-peer-deps was rejected because it masks future genuine peer conflicts and still reifies the full tree. The same step's SDK version is aligned from 0.31.0 back to the 0.29.1 that package.json and check:guards enforce after the streaming outage. That drift went unnoticed because the pin guard only inspects package.json and the lockfile, never workflow files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * build(docker): generate crontabs from vercel.json vercel.json defines 16 cron jobs; both Docker crontabs carried 9, and were byte identical to each other. Self hosted deployments therefore never sent recurring invoices, never dispatched webhooks and never cleaned up idempotency keys. tax-deadlines also ran once a year on 2 January instead of daily, and documents/verify weekly instead of daily. Extension crons are included rather than excluded. The Dockerfile copies the whole tree before building, so every extension cron route is compiled into the image regardless of the enabled preset, and each returns 200 when its extension is unconfigured, so curl -sf logs no failure. Two such entries were already present in the crontab for extensions absent from the preset, which settles the intent. documents/verify is treated as drift rather than a self hosted concession: the weekly cadence was present in the hosted crontab too, and the run is capped at 200 documents walking a nulls-first queue, so weekly drains the integrity queue seven times slower on a check that exists for BFL retention. webhooks/dispatch keeps its per minute cadence, adding 1,440 requests a day on self hosted. A gentler tick would silently stretch the first retry, since the retry ladder opens at 60 seconds. SCHEDULE_OVERRIDES is the one line place to change that. A parity test asserts the path sets match minus a documented exclusion list, and ratchets three cron routes that are currently scheduled nowhere so they are named rather than silently rotting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(observability): add a provider agnostic error sink There is no error tracking in this codebase: logs go to console and Vercel retention and nowhere else, nothing alerts on the 16 cron jobs, and seven code comments across lib, app, components and extensions asserted that Sentry captures errors when Sentry is not a dependency. The two most recent bug fixes on this repo were both discovered by customer email. This adds the sink, not a vendor. No dependency is taken: the interface has a no-op default and a registration point, so behaviour is unchanged until an adapter is registered. Releases are tagged from the build id already inlined by next.config.ts. Redaction moved out of lib/logger.ts into a leaf module that both the logger and the sink import, so there is one denylist and no path from application data to a third party can skip the personnummer regex, including direct sink calls that bypass the logger. That matters here because these logs carry personnummer and financial data. verifyCronSecret now reports its own 401s, which covers all 16 jobs without touching a route file and catches the case where CRON_SECRET is rotated without updating the scheduler and every job silently 401s forever. The threshold is one failure rather than the backup alert's three: suppressing the first occurrence is precisely how an outage stays invisible. The seven misleading comments are corrected to describe what the code actually does, including the two cases that still are not covered: the client side one, since the sink is server side, and a warn level call that is not forwarded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: remediate the 2026-07-26 similar-sweep findings across all surfaces Resolves the ~150-finding sweep (dev_docs/similar-sweep-2026-07-26.md) with one agent per finding; every behavioural fix carries a regression test proven to fail at HEAD. Full status, corrections to the sweep, refusals and open decisions in dev_docs/similar-sweep-2026-07-26-remediation-status.md. Structural roots closed: - resolveSekAmountOrNull(): honest SEK resolution refuses instead of booking 1:1; four duplicated toSek closures now refuse via INVOICE_FX_RATE_MISSING - ledger-line-amount.ts: journal_entry_lines.currency labels the document, not the amount; SQL pre-filter decoy proven and fixed - sparse-patch.ts: .partial() does not strip .default() in Zod 4.4.3; the exploitable salary payslip-line PATCH and KPI preferences sinks fixed - tests/schema: migration-replay phantom-column guard (13k+ refs, closed CHECK sets, onConflict targets); found 28 real defects, all fixed, all four baselines now empty - three new ratchet guards: sek-labelled-amount, cross-extension-import, ungated-extension-route Highlights: lawful VAT-rate set on all seven invoice surfaces (ML 6 kap), RC input VAT mismatch wired on web + both MCP callers, missing-underlag resource delegates to the shared RPC predicate, push-notifications consent polarity fail-closed, deadlines undo honours requested state, silent-failure and read-side-fabrication classes fixed across settings/KPI/inbox/Stripe/ Arcim/kassaflodesanalys, error-envelope stringification fixed at 10+ sites with isSwedishUserMessage extended. Also includes the parallel session's MCP invoice tools (update_invoice, recurring schedules, invoice deliveries) which share files with the sweep work and are verified green together. 13 new migrations are NOT applied anywhere; they apply via branch merge. 20260726120000 backfills 1247 supplier-invoice rows. pg tests for new DDL are written but unrun (no local Postgres). Verified: 11088 tests / 881 files green, tsc 0 non-test errors, lint 0 errors, check:guards passing, MCP payload 57475/57500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): rename replace_sie_import migration off main's 20260726090000 version origin/main shipped 20260726090000_agent_quota_rpc_caller_guard.sql; keeping our replace_sie_import migration on the same version would abort the Supabase apply with a schema_migrations_pkey duplicate at merge time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): remediate pre-publish deep-review findings across all slices A 13-agent review of the full branch diff surfaced 1 critical, 5 high and ~45 further findings; this commit resolves them in one pass: - replace_sie_import / undo_sie_import: p_user_id honored only for service_role callers; any other caller is pinned to auth.uid() (impersonation gate bypass), authz raise errcode 42501 mapped to a Swedish 403 in the route, new caller-guard migration for undo - bulk_book_transactions refuses homogeneous non-SEK batches instead of writing foreign magnitudes into SEK ledger columns - credit-note cap trigger: company-match on credited_invoice_id, no cross-tenant figures in exception text - link_voucher RPCs resolve NULL invoice currency as SEK end to end - personal-number ciphertext CHECK split into NOT VALID + VALIDATE - same-currency foreign settlements clear 1510 at booking rate and book realized diff to 3960/7960; rate-less foreign write paths refuse - receivables revaluation covers partially_paid and outstanding amounts - period lock guard paginates candidates past the PostgREST 1000 cap - documents: service-client storage removals after authz, dual-layout reads in integrity cron and archive export, backfill delete-source sweep actually deletes with hash verification and shared-key grouping - invoice matching normalizes NULL/lowercase currencies (regression), duplicate candidates stop claiming amount matches they never ran - match-invoice aborts on any booking failure (no paid-without-verifikat) - refresh-exchange-rate reverts on concurrent booking (TOCTOU window) - KPI preferences upsert arbiter aligned to the company-scoped constraint - personnummer_last4 stripped from all salary responses incl. MCP tools - worked-hours batch restores destroyed rows on conflict and error paths - MCP: shared duplicate-claim builder (no more 'null kr'), short-circuit on tag_journal_lines overflow, auto_send schedules stage as high risk - observability sink redacts emails/IBANs/API keys and keeps redacted stacks in prod; assorted small guards (safe-return-to /@, dry_run=True, cursor helper off-by-one, OAuth state TTL 10 min, arcim saveMappings call removed) Full dispositions, deferred items and hand-verified accounting numbers are documented in the PR body and DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(personnummer): implement masking and encryption for personal numbers with tests * fix(review): address CI and compliance-bot findings for PR #1215 pg-real: the CI image's auth shim reads the legacy request.jwt.claim.role GUC, so both service-role simulations (runAsServiceRole and the invoice-delivery test's local helper) never satisfied auth.role() = 'service_role' and every legitimate p_user_id path failed closed; the shared helper now sets both GUC shapes plus SET LOCAL ROLE with a fail-loud sanity check, and the delivery test reuses it. The link-voucher migration had recreated both RPCs from pre-rewrite file text, reintroducing the NULL-unsafe membership pattern the null-safe-tenant-guards ratchet bans; both guards now use public.caller_is_company_member() with all currency changes preserved. Compliance bots: the customers export now emits the standard masked form instead of raw AES-256-GCM ciphertext in the Org-/personnummer column, and maskCustomerRow returns a non-round-trippable placeholder on decrypt failure instead of 500ing the list. MCP parity: gnubok_lock_period's staging pre-check now runs the exact countUnbookedInPeriod the commit path enforces (exported from period-service; local mirror deleted), and gnubok_agi_status resolves AGI state run-scoped so a correction run no longer renders as already filed. Declined with evidence: PR-Agent's opening-balances null-zeroing concern (all mergeable columns are NOT NULL with defaults per 20260713101000). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address codex review findings on PR #1215 - restore 20260726140000 to its preview-recorded content and restate the NULL-safe tenant guard under 20260727130000: a recorded migration version never re-runs, so the in-place edit could not reach the preview branch - replace toFixed() with sv-SE two-decimal formatting in the ROT/RUT cap warning texts and update the pinned test expectations - drop the em dash in the fiscal-periods route comment - strip trailing whitespace in import-existing.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): raise timeout on real PDF render tests renderToBuffer does real @react-pdf layout work and exceeds the 5s default when the full suite saturates the CPU; tests pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
d840257c0c |
Add/stripe connect transactions (#1139)
* fix(mcp-oauth): allow ChatGPT connector callbacks and resume OAuth after login Add chatgpt.com/connector/oauth/* (per-instance) and the legacy chatgpt.com/connector_platform_oauth_redirect to the built-in OAuth redirect allowlist so ChatGPT MCP connectors can register and authorize. Fix the login page dropping the ?next= destination: an OAuth-initiated visit that required login previously ended on the dashboard and the connection flow silently died. Login now resumes to the sanitized next path (hard navigation, since the consent page is route-handler HTML), carries it through the MFA step-up as returnTo, and /mfa/verify hard-navigates for /api/ destinations. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): dedup incoming feed rows against booked hand-entered twins Users who bookkeep via MCP/chat first and connect their bank afterwards got the same movement twice: the synced row's external_id lives in a different namespace, the free-form manual title never text-bridges the bank's raw string, and the cross-channel mirror deliberately excluded manual/mcp rows. Extend the mirror with a booked-hand-entered track: an incoming feed row is skipped when a BOOKED manual/mcp row shares its (date, ore) bucket count- symmetrically. Gates beyond the feed-vs-feed mirror: stored row must be booked (staged rows never consume an import), currencies must not contradict (bucket key is date+ore only), the cash-account guard applies to the count exactly as to consumption, and symmetry uses the Layer-1-unmatched incoming count so an already-stored row cannot inflate it. Consumption stamps the batch cash_account_id onto an account-unbound hand row, so one hand row can never consume feed rows on other accounts in later syncs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(bookkeeping): inline verifikat rattelse (strike lines + text/date edit) Second sanctioned correction track under BFL 5 kap 5/9 pp, Fortnox-style: strike lines inside a posted verifikat with replacements in the same voucher, and correct description/entry_date without an andringsverifikat. Envelope: posted entries, open unlocked periods, company lock date, same-period date moves, structural/FX/doc-linked lines excluded, and a reconciliation guard preserving per-account net on bank/reskontra sides of externally linked entries. Every rattelse writes an immutable who/when row (journal_entry_rattelse_log, WORM, archived as rakenskapsinformation) and struck originals render struck-through in the verifikat; list rows and the detail header carry a Rattad marker. CLAUDE.md hard rule 1 and the swedish-accounting-compliance skill are amended to state the two-track rule. Staging carries the DDL; prod gets it on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: live saldo in booking form, prior-year window comparison, hideable assistant FAB - Manual journal entry: saldo column now shows before -> after computed from the typed debit/credit amounts (direction feedback while booking) - Resultatrapport: a narrowed date range now compares against the same window shifted one year back (#862), merged across fiscal periods for brutet rakenskapsar; P&L rows report window activity instead of rolled-forward YTD closing - Assistant FAB: per-user hide toggle (user_preferences.hide_assistant_fab, settings > assistant), sidebar entry unaffected; collapsed sessions keep their reopen handle Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(stripe): sync balance transactions as a bank feed on 1686 Import the connected Stripe balance into the transactions inbox, opt-in per connection (transaction_sync_enabled on stripe_connections): - Balance transactions map to feed rows with the two-row gross+fee split and frozen external_id formats (stripe_{acct}_{txn} / _fee), dated on created, bound to a provisioned "Stripe-saldo" cash account on 1686 so booking settles against the clearing account by construction. - Double-booking protection: settled payment-link charges import pre-linked to their settlement entry; payout rows import pre-linked to the payout entry; processPayoutPaidEvent claims the payout's fee rows at booking time (linkPayoutFeedRows, idempotent from both directions). - Cursor last_balance_txn_synced_at with 24h overlap; first run backfills 90 days floored at the day after the company lock date. - Nightly cron /api/extensions/stripe/transactions/cron (03:30), transaction-sync toggle route, "Synka nu" covers both feeds, settings panel toggle with last-synced/backfill note, sv+en strings. - Migration 20260723200000 (applied to staging). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(transactions): offer match-to-voucher on unbooked history rows Unbooked transactions with is_business already set (e.g. left behind when a voucher was removed without a full uncategorize) land in the history list instead of the inbox, where the match-against-existing-voucher action did not exist, leaving them with no path back to voucher matching. Add the same menu item to the history list for unbooked rows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(transactions): enhance ownership checks and error handling in journal entry routes --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
288915c152 |
Fix/fdb fr usrs (#1125)
* fix(invoices): return attachment filename in delivery history summaries The 20260723003000 hardening dropped attachment_filename from list_invoice_delivery_summaries, so the delivery history UI always fell back to the generic "faktura.pdf" label. Recreate the RPC with the filename included: it is derived from company name, customer name, invoice number, and date, all already visible to every company member, so the minimization boundary is unchanged. Addresses stay masked and message content, BCC, and checksums stay server-side. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(reconciliation): surface own-account transfer legs in match-to-voucher by default The second (incoming) leg of a transfer between two of the company's own bank accounts was hidden in the 'Matcha mot befintlig verifikation' dialog because the voucher counted as 'already matched' once its outgoing leg was linked, even though the incoming account's line had no settling transaction. Users read the empty default list as 'the app won't let me link this'. get_account_gl_lines_for_matching now counts links per settlement account: a transaction provably on another cash account no longer marks the voucher as matched for the requested account, so the unsettled transfer leg surfaces by default (and auto-selects on an exact match). Same-account N:1 stays behind the 'Visa aven matchade verifikationer' opt-in, and transactions without a resolvable cash account conservatively keep counting everywhere. get_unlinked_gl_lines is deliberately untouched (feeds auto-reconcile). Companion guard: mark_entry_as_opening_balance now refuses entries with linked bank transactions, since half-settled transfer vouchers became reachable in the reconciliation view's unmatched table where 'Mark som IB' renders; re-tagging one would strand its transaction against a movement- excluded entry. getReconciliationStatus counts unmatched GL lines with the account-scoped RPC so the status card agrees with the table. Fixes #1026 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(api): cut prod p95 latency via local JWT auth, single-RT company resolution, and report aggregate RPCs Baseline 2026-07-23 (487 prod samples): p50 160ms, p95 480ms, 13% of requests over 300ms. Target: p95 under 300ms. - requireAuth: verify JWTs locally via getClaims (ES256/JWKS) instead of a second network getUser per request; getUser fallback keeps HS256 self-hosted and existing test mocks working; middleware still revocation-checks every /api request - resolve_active_company RPC (20260723161000): one round trip replaces 2-3 queries in getActiveCompanyId and middleware; PGRST202/42501 fall back to the legacy query path - arsredovisning build-data: ~33 sequential round trips down to ~7, output byte-identical (snapshot-proven) - currency rate route: stop bypassing the exchange_rates cache (missing supabase arg caused an external Riksbanken call on every request) - document.get: parallelize row fetch, signed URL and audit event - list_company_accounts RPC (20260723170000): accounts list in one round trip instead of paging past PostgREST's 1000-row cap - vat-declaration route: drop a dead sequential company_settings query - get_kpi_report_aggregates RPC (20260723180000): KPI report's three full-period line scans collapsed into one aggregate call; dimension- filtered path unchanged - lint: fix 9 baseline errors, downgrade 4 react-hooks compiler rules to warn, zero the eslint baseline ratchet All four gates green: lint 0 errors, 9163 tests, check:guards, build. Migrations applied idempotently to staging only; prod receives them via Supabase branching on merge. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): resolve PR review findings across auth, VAT declaration, and IB retag - requireAuth getClaims fast path: pin iss (project URL) and aud ('authenticated'), log every fallback to getUser (ASVS V9.1 finding) - remove the ignored accountingMethod parameter from calculateVatDeclaration and the dead company_settings.accounting_method reads in xlsx/pdf/eskd routes; v1 API keeps accepting the query param but documents it as a no-op - close the mark_entry_as_opening_balance TOCTOU race with a transactions trigger (20260723190000, FOR KEY SHARE on journal_entries) + pg tests; applied to staging and smoke-verified both directions - re-add the 42501 tenant guard to branch-local migration 20260723160000 (function body had silently reverted to the pre-20260619130100 definition) - document the buildK3Noter tbFullRows full-TB contract (uppskjuten skatt opening balance per BFNAR 2012:1 ch.29) - add KPI VAT-liability test covering reduced-rate output accounts 2621/2631 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(db): use NULL-safe caller_is_company_member in opening-balance retag guard The re-added tenant guard carried the pre-20260703180000 raw NOT IN (SELECT user_company_ids()) pattern, which the null-safe-tenant-guards ratchet blocks. Staging re-synced. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
237b77a366 |
feat: custom inbound mail domains, rot/rut payout file, invoice email texts, security hardening (#878)
* fix(security): guard MCP test keys, RLS role gate + voucher RPC guards, /api MFA gate, deps - MCP: force dry-run / block writes for test-mode API keys in tools/call (extensions/general/mcp-server) - DB: current_user_can_write role gate on write policies (40 tables) + tenant guards, SET search_path, REVOKE anon on commit_journal_entry / next_voucher_number / detect_voucher_gaps (migration 20260702093000) - Middleware: MFA (AAL2) gate on cookie-authenticated /api routes via apiPathSkipsMfaGate - Deps: npm audit fix clears mailparser/linkify-it/nodemailer/svix/uuid highs; xlsx -> SheetJS 0.20.3 Adds unit + pg-real tests. Does not touch in-progress ROT/RUT or invoice-email-texts work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): rot/rut begäran om utbetalning — HUS XML (V6), payout tracking + settlement, MCP tool Generates Skatteverkets begäran-om-utbetalning file (schema V6) from paid ROT/RUT invoices — no submission API exists, the file is uploaded manually at skatteverket.se. Headless by design for now: API routes + MCP tool (gnubok_generate_rot_rut_file), no UI surfaces. - lib/invoices/rot-rut-file.ts: pure XML generator with deterministic per-invoice blockers (hours, work type, personnummer, property info, mixed rot+rut, XSD limits) + 31 January deadline warnings - rot_rut_payout_requests(+items) tables: one active begäran per invoice (DB triggers incl. reactivation guard), RLS, audit, pg-real tests - Settlement: POST /settle books debit 1930 / credit 1513 via the engine (source_type rot_rut_payout); partial payouts → partially_paid - Work-type lists corrected against Begaran.xsd: IT-tjänster is rut-only, snöskottning/tillsyn/tvätt added (schablontjänster utfört-only) - Fix: invoice-level fastighetsbeteckning was validated but never persisted — now stamped onto rot lines in build-invoice-write; API accepts bostadsrätt pair (lägenhetsnr + BRF orgnr, editor UI deferred) - invoice_items.brf_org_number migration + MCP scope invoices:write Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(invoices): per-company editable invoice email texts Add an "E-posttexter" section under Settings -> Fakturering where the subject, greeting, body and sign-off of the standard invoice email can be customized per company in Swedish and English. Fields pre-fill with the standard texts and only diffs from the standard are stored (company_settings.invoice_email_texts JSONB), so future improvements to the stock wording still reach companies that have not customized. Each field has a reset-to-standard button; cleared fields snap back. Texts support a fixed placeholder set (invoice number, customer name, first name, company, due date, amount) substituted at send time in a single pass; unknown placeholders stay literal. Custom texts are HTML-escaped after substitution, newlines become <br> in the HTML variant, and subject lines are flattened to a single header line. Overrides apply to standard invoices only - credit notes, proforma and delivery notes keep the stock texts. All send paths (UI, v1 API, MCP approval, recurring) pick the texts up via the existing settings row. The Zod schema half of this change (InvoiceEmailTextsSchema in lib/api/schemas.ts) was inadvertently included in 8291f745. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(documents): accept PDFs with preamble before %PDF- header, surface content rejections as 400 detectFileMagic required the %PDF- signature at byte 0 (BOM aside), rejecting genuine PDFs that carry a leading newline or junk bytes — files every ISO 32000 reader opens fine. Now scan the first 1024 bytes for the signature, matching real-reader behavior. Image types stay strict at offset 0 to keep the anti-placeholder defense tight. Magic-byte rejections were also mislabeled as DOC_UPLOAD_STORAGE_FAILED (500 'Filen kunde inte sparas'), blaming storage for a client-side file problem. Both upload routes now map them to a new DOC_UPLOAD_INVALID_CONTENT (400) with an accurate message. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(bookkeeping): full keyboard flow for manual journal entry Enter now drives the whole verifikat flow: verifikationstext drops into the first row missing an account, konto commits advance to debet, Enter on an empty debet hops to kredit, and an entered amount jumps to the next row. Once the voucher balances, Enter opens the review (unchanged gate) and the auto-focused confirm posts it — including through the no-underlag warning dialog. Escape in the inline review goes back to the form. Also fixes an Enter footgun in AccountCombobox: a bare Enter on a freshly focused field no longer selects the first account in the list — selection now requires typing or arrow navigation; otherwise Enter re-commits the current value or bubbles to the form-level handler. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat: add custom inbound domains management for companies - Implemented functionality to allow companies to claim and manage their own inbound email domains via Resend's API. - Created a new table `company_inbound_domains` to store domain information, including status and DNS records. - Added necessary RLS policies to restrict access based on user roles (owner/admin). - Developed functions for domain normalization, validation, claiming, verification, and removal. - Implemented webhook handling for domain status updates from Resend. - Added comprehensive tests for RLS, constraints, and triggers related to the new domain management feature. * fix: address PR #878 review findings and CI failures - migrations: drop the ai_usage_tracking policy block from the role-gate migration — the table was removed by 20260504120000_remove_ai_subsystem and only lingers on staging as drift; a from-scratch chain (pg-real, Supabase preview) failed on it - invoice-inbox: never flip a custom domain to verified off a domain.updated webhook alone — confirm the receiving capability with Resend first (fail-closed); normalize both sides of the orphan-adoption domain match - rot/rut: block files where begärt belopp exceeds what the buyer paid (DEDUCTION_EXCEEDS_PAYMENT); tighten brf_org_number validation to real orgnr shapes; parameterize the settlement bank account (19xx, default 1930) - rot/rut routes: log acting user on financial mutations, stop swallowing item mirror errors, narrow response projections (no customer ids through the invoice join); document the deliberate inline-XML decision - documents: stop echoing raw storage-layer error messages to clients Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: round-2 CI + compliance findings on PR #878 - migrations: the role-gate migration targeted automation_webhooks, which 20260515170000_webhooks_v2 renamed to webhooks on the canonical chain (staging kept the old name — drift); gate public.webhooks instead, dropping legacy schema-sync policy names defensively. Restore the 20260623130000 owner fallback in next_voucher_number that the stale copied-verbatim body silently reverted (caught by engine.pg locally). Full migration chain verified from scratch against supabase/postgres:15. - mcp: bump the tools/list payload ceiling 44K -> 45K — main's #877 qualified-identifier schemas plus this branch's rot/rut tool crossed the ceiling only in combination; documented in the test's history log. - rot/rut: refuse partial settlement before Skatteverkets beslut is recorded (would bypass the PATCH lifecycle and strand the request); block zero-kronor ärenden (ZERO_DEDUCTION); require sekelsiffra 16 on 12-digit brf orgnr in both schema validation and normalizeBrfOrgNr Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix: rename branch migrations off main's colliding versions After the merge with main, two versions were shared by two files each (20260702100000: rot_rut_payout_requests vs company_settings_dimensions_ enabled; 20260702130000: invoice_email_texts vs pending_operations_add_ create_dimension_value). psql-based CI applies by filename and doesn't care, but Supabase branching records migrations by version (PK) — the second file with the same version breaks the preview with a schema_migrations_pkey duplicate. Neither branch migration is version- recorded on staging or prod, so renaming to fresh 20260703 versions is safe; nothing between the old and new positions depends on these objects. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(security): scope the /api MFA-gate bypass to real Bearer-auth surfaces Any Authorization header — attacker-controlled — used to skip the AAL2 gate for every /api route, so a stolen-password AAL1 cookie session could reach cookie-authenticated routes (which ignore the header) by attaching `Authorization: x`. The skip is now scoped to the surfaces whose auth contract IS the header (/api/v1 API keys, the MCP endpoint's OAuth tokens); pure Bearer callers elsewhere (cron secret, signed webhooks) carry no cookie session and were never touched by the gate, which only fires for cookie users. Superagent P2 on PR #878. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test: normalize path separators in dimension statutory guard scan The route scan compared walked file paths against a POSIX-path allowlist, so the suite failed on Windows (backslash separators) while passing on Linux CI. Normalize the scanned paths to forward slashes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
0bc81d4c88 |
feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools (P0-3) (#681)
* feat(auth): SoD acknowledge on stage+approve keys + agent:write scope for memory tools Segregation of duties on API keys is now warn + explicit acknowledgement (not block): minting a key with any staging write scope AND pending_operations:approve returns 409 API_KEY_SOD_CONFLICT unless the caller re-POSTs with acknowledge_sod: true. The acknowledgement is recorded (sod_acknowledged_at / sod_acknowledged_by) for an auditable risk acceptance (ISO 27001:2022 A.5.3 / BFNAR 2013:2). The create UI surfaces an inline warning and an explicit confirm dialog before submitting the ack — the default "all scopes ticked" create routes through that path. Also introduces the agent:write scope and maps the previously-UNMAPPED memory tools gnubok_remember_fact / gnubok_forget_fact to it. Because unmapped tools were callable by any key, the migration grandfathers agent:write onto every existing non-revoked key with an explicit scope list so nothing regresses; new keys must opt in. agent:write is deliberately excluded from the default grants and is NOT a staging scope (no SoD conflict with approve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(db): enforce both-or-neither on the SoD acknowledgement pair Review finding (Greptile P2): sod_acknowledged_at/sod_acknowledged_by were independently nullable, so a partial write could silently pass and undermine the auditable risk acceptance (ISO 27001 A.5.3 / SOC 2 CC6.1). Adds a paired-NULL CHECK constraint + pg-real coverage for both partial-write directions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(auth)+feat(auth): compliance-review round — self-attestation documented, ack logged, SoD boundary assumption captured - Migration header now states explicitly that the SoD acknowledgement is a SELF-attestation by deliberate design (enskild firma has no second person; the claude.ai approval flow needs stage+approve on one credential) — the control objective is informed consent + audit record, not dual control. - The acknowledge_sod=true path now emits a structured log.warn (api_key.sod_acknowledged with key id/prefix, conflicting scope, scopes, acknowledger, company) so the acceptance lands in the logging pipeline in addition to the sod_acknowledged_* columns (ASVS V16.1.1). - STAGING_SCOPES carries the documented system control (BFNAR 2013:2 systemdokumentation) for why agent:write is not a staging scope: memory tools write advisory agent context and cannot stage räkenskapsinformation. Dismissed as by-design/verified: hard-block and second-approver remediations (user decision: warn + acknowledge); scope-update gap (the [id] route only supports DELETE — scopes are immutable post-creation); session-auth concern (withRouteContext is cookie+MFA only; API-key auth exists only on /api/v1 and MCP). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: re-trigger CI (Supabase Preview 502 infra hiccup) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
32d9978f1b |
Fix/chrome pdf preview csp (#572)
* feat: add option to exclude year-end closing entries in SIE export and related reports * delete docs * fix: allow Chrome's PDF viewer in verifikat document preview The /api/documents/:id/inline route shipped with `object-src 'none'` in its CSP, which blocked Chrome's built-in PDF viewer (it renders inline PDFs via an internal <embed>). Users on Chrome saw "Det här innehållet har blockerats" when expanding a PDF attachment in the bookkeeping view; Firefox (PDF.js) and Edge (own viewer) were unaffected, and JPGs worked because <img> isn't subject to object-src. Drops the CSP for this route to the minimum needed for embeddability: `frame-ancestors 'self'`. X-Content-Type-Options: nosniff plus the fixed Content-Type from the handler already block MIME confusion; X-Frame-Options: SAMEORIGIN + frame-ancestors still block clickjacking. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(auth): add webmail deep link to email confirmation screens Mirrors Stripe's signup UX: after asking the user to verify their email, detect their webmail provider from the domain and show a button that opens the inbox in a new tab. Gmail gets a from:<sender> search pre-populated; Outlook/Yahoo/iCloud/Proton open the inbox directly. Unknown / custom domains fall back to the existing copy. Sender address is configurable via NEXT_PUBLIC_BRANDING_AUTH_EMAIL_FROM (default noreply@gnubok.se) so white-label installs can match their Supabase Auth SMTP config. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): unblock first-time password set for BankID users with MFA Supabase rejects updateUser({password}) and mfa.unenroll with "AAL2 session is required" whenever a TOTP factor is enrolled. BankID magic-link logins produce AAL1, and middleware skips MFA enforcement for bankid_linked users, so they had no path to AAL2 — leaving them unable to set a backup password or disable MFA without going through the email-recovery escape hatch. - /api/account/password: branch on app_metadata.has_password. First-time set writes via service.auth.admin.updateUserById (no existing credential to protect, AAL2 guard does not apply). Change-password keeps the user-session updateUser so AAL2 still fires for credential rotation. - /mfa/verify: accept a safeReturnTo query param and route there after successful verify, so step-up flows can land back where they came from. - SecuritySettings: detect the AAL2 error from both change-password and mfa.unenroll and redirect through /mfa/verify?returnTo=/settings/account instead of toasting a dead-end error. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add tests and rounding utility for öre precision in bokslut calculations - Implemented `roundOre` function for rounding SEK amounts to two decimal places, ensuring consistent monetary calculations. - Introduced `ORE_TOLERANCE` constant for comparing rounded amounts, facilitating invariant checks in financial entries. - Created comprehensive tests for `roundOre`, covering typical cases, edge cases, and idempotency. - Added year-end invariants tests to verify database-level guarantees for closing entries, ensuring they balance to the öre and reject discrepancies. - Developed end-to-end tests for the dispositions chain, validating the correctness of calculations across various scenarios. * fix: update PDF rendering to remove Swish QR code generation and set default to disable Swish visibility * fix: enhance security by rejecting data URIs in safeReturnTo function tests * fix: improve rounding logic in roundOre function and add customer_type migration * fix: add customer_type column to customers and enforce CHECK constraint --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cc351158f8 |
Invoicing & account-security polish bundle (#550)
* feat: invoicing & account-security polish bundle Five independent improvements bundled to ship together: - BankID/password lockout fix: BankID-only users could enroll MFA and brick themselves (Supabase requires AAL2 to change password or unenroll MFA, and AAL2 needs a password sign-in). New app_metadata.has_password flag tracks this; middleware gates /mfa/enroll behind it, /account/set- password is the unlock path, SecuritySettings shows a banner, and /api/account/password is the single write path that flips the flag. Backfill script for existing users. - Swish invoice payment method: company_settings.swish + invoice_show_swish columns, validation in lib/api/schemas.ts (accepts 123XXXXXXX företag or 07XXXXXXXX mobile, strips whitespace/hyphens), rendered on invoice PDFs. - Send-reminders kill switch: per-company company_settings.send_invoice_ reminders toggle in PdfPrintSettings/Automatisering. Reminder processor also tightened: positive status allowlist (sent + overdue) so terminal statuses can never match; skip when customer already responded via reminder link; race-window re-check before send. - First-invoice logo prompt: one-shot dialog when creating the first invoice without a logo (issue #520). Self-limits via head-only count. - SIE export opening-balance fallback: route IB through getOpeningBalances so the compute_prior_opening_balances RPC supplies #IB after multi-year imports where opening_balance_entry_id is intentionally NULL. Previously #IB silently went to zero and #UB collapsed to current-period movements. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(account-polish): address PR review feedback - BankID-link path (extensions/general/tic/index.ts): read-merge-write app_metadata instead of passing { bankid_linked: true } alone. updateUserById REPLACES app_metadata wholesale, so the previous code would have wiped has_password for any user who later linked BankID, causing the set-password banner to (incorrectly) reappear and blocking the standard MFA enrollment button. The comment is now corrected. - Middleware (lib/supabase/middleware.ts): thread inner returnTo through the /mfa/enroll → /account/set-password redirect so the user lands on their original destination after the full chain completes, not on /. - safeReturnTo helper (lib/auth/safe-return-to.ts): replace the starts-with-/-but-not-// guard on mfa/enroll and set-password pages. The previous guard let /\evil.com and /@evil.com through. The new helper parses against a synthetic base origin and verifies it matches. - set-password page (app/(auth)/account/set-password/page.tsx): remove CLAUDE.md design system violations — bg-gradient-to-b on page bg, inline shadow-md style on the card, space-y-5, font-medium on the h1, rounded-xl on the card. Flat surface, hairline border, font-display h1 per the design tokens. - Swish dedup (lib/payments/swish.ts): extract normaliseSwish() and isValidSwish() helpers and use them in lib/api/schemas.ts, components/settings/BankDetailsForm.tsx, and the invoicing settings page. Single source of truth for the regex. - Password route (app/api/account/password/route.ts): emit a structured success log so the audit pipeline can detect password-set events, not just failures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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. |
||
|
|
db592d922d |
feat(api): v1 REST API foundation — auth wrapper, scopes, registry, smoke endpoints (#450)
* feat(api): v1 REST API foundation — auth wrapper, scopes, registry, smoke endpoints Lay the substrate for the public REST API at /api/v1/*: Bearer-auth wrapper that reuses the existing api_keys + idempotency machinery, an extended scope catalogue (companies, events, webhooks, operations, documents, compliance), v1 response envelopes (data + meta with request_id, api_version, audit block, cursor pagination), an error envelope with recovery_hint / docs_url / valid_alternatives derived from the existing structured-error registry, and a Zod schema registry that generates the OpenAPI 3.1 spec with x-action-risk / x-idempotent / x-reversible / x-dry-run-supported extensions. Ships discovery routes (/llms.txt, /.well-known/skills/index.json) and three smoke endpoints (GET /api/v1/health, /api/v1/companies, /api/v1/openapi.json) so the wrapper is exercised end-to-end. Includes the api_keys.mode (test|live) migration and 41 unit tests covering auth, scope, company-membership, idempotency replay, dry-run, pagination, response shape, and scope resolution. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): harden v1 foundation — cursor validation, security headers, forensic logs Address compliance-swarm findings on PR #450: - OWASP V2.3: decodeDefaultCursor now validates the cursor's ts as ISO 8601 and id as UUID. A crafted cursor previously could inject untyped strings into a query's .gt(field, value); PostgREST would have rejected them, but validating here keeps the failure mode predictable (stale cursor → reset) rather than 400-ing. - OWASP V3.4: public discovery routes (llms.txt, .well-known/skills, openapi.json) now stamp X-Content-Type-Options: nosniff, Referrer-Policy, X-Frame-Options: DENY. New lib/api/v1/security-headers.ts helper. - OWASP V16: security event logs (missing token, validation failure, insufficient scope, company-membership deny) now include source IP (x-forwarded-for / x-real-ip) and User-Agent for forensic correlation. - OWASP V8.2.1 / ISO A.8.3: GET /api/v1/companies emits a warn log when the PostgREST archived_at filter unexpectedly returns a row with a null company join, surfacing silent data-integrity regressions instead of hiding them behind the existing pickCompany() === null filter. Pushing back on (not changed): - GDPR Art.32 cursor HMAC signing — cursors only paginate within a user's own user_id scope; cross-tenant probe surface doesn't exist yet. - GDPR Art.25 org_number in list — Bolagsverket public-record data, removing forces N+1 fetches to make the response useful. - SOC 2 CC6.3 service-role bypasses RLS — defense-in-depth IS the design; the wrapper's company_members membership check is the technical control. - ISO A.8.12 public OpenAPI spec — intentional, mirrors Stripe/Twilio. 5 new pagination tests cover the cursor validators. 46/46 v1 tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(auth): detect Supabase duplicate-signup obfuscation on register Supabase obfuscates duplicate signups to prevent user enumeration: when an email already belongs to a confirmed account, signUp returns data.user with identities: [] and no error, and sends no email. Without detecting this case we showed the "check your email" screen to the user, who then waited for a mail that never arrived. Detect the empty-identities response and surface it via duplicateEmail state so the UI can branch on it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): second-pass hardening — CSP, IP truncation, cursor scope comment Address the second compliance-swarm sweep on PR #450: - OWASP V3.2: PUBLIC_SECURITY_HEADERS now includes Content-Security-Policy default-src 'none'; frame-ancestors 'none'. Free win for JSON/text-only public routes (no script, style, image, or form contexts). - GDPR Art.5(1)(f): truncate IPs before logging — IPv4 to /24, IPv6 to /48. Preserves diagnostic value (ASN, abuse-pattern correlation, city-level geolocation) while eliminating point-of-presence identification. Standard pattern used by Google Analytics anonymize_ip. Exported truncateIp() so other surfaces can adopt it. - OWASP V8.2.1: explicit comment in GET /api/v1/companies documenting that the cursor's joined_at is applied AFTER user_id filter, so a tampered cursor can only reorder rows the caller already owns. Cursors deliberately unsigned; trade-off documented. Pushing back on second-pass findings (not changed): - ISO A.8.12 / SOC 2 CC6.3 health/llms.txt/skills exposing service name + API version + MCP URL — these are intentional disclosures for a public 3rd-party developer API; hiding them is theatre. - GDPR Art.32 logging granted scopes on INSUFFICIENT_SCOPE — diagnostic value during incident response outweighs the theoretical privilege-profile leak; an attacker who already breached the log store has bigger problems. - OWASP V2.2 route-level Zod for cursor — decodeDefaultCursor already validates strictly; route-level Zod is stylistic. - GDPR Art.25(2) org_number/entity_type in list — Bolagsverket-public data; entity_type materially affects which API calls make sense. - ISO A.8.15 x-forwarded-for trusted-proxy CIDR — overkill behind Vercel's edge which rewrites the leftmost value. 50/50 v1 tests pass (4 new for truncateIp). Build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): third-pass hardening — Host header injection, anon client, HSTS Address the third compliance-swarm sweep on PR #450: - SOC 2 CC6.1 (3× high): llms.txt, openapi.json, and .well-known/skills built URLs from the inbound Host header. A spoofed Host could poison agent discovery with attacker-controlled endpoints. New lib/api/v1/base-url.ts centralises canonical base-URL derivation via NEXT_PUBLIC_APP_URL (already a required env var per CLAUDE.md). - ISO A.8.2 / A.8.5 (2× high): the wrapper's public-scope code path now uses an anon-key Supabase client (RLS-respecting) instead of the service-role client. A future accidental DB call from a public handler is constrained to anon-accessible rows. Least-privilege at the infrastructure layer. - OWASP V3.2 (medium): PUBLIC_SECURITY_HEADERS now includes Strict-Transport-Security: max-age=31536000; includeSubDomains. - GDPR Art.5(1)(f) (medium): truncateIp now logs a warn when a non-empty x-forwarded-for / x-real-ip payload fails to parse, surfacing spoofed or unexpected proxy values to security monitoring instead of silently dropping them. The raw value is never logged. - CC2.3 (low): llms.txt now links the SECURITY.md disclosure policy with the security@arcim.io reporting address so agents have a clear responsible-disclosure path. Pushing back on third-pass findings (not changed): - Cursor HMAC signing — user_id filter is the authorisation boundary; cursor scope is bounded to within-user rows. Documented in code. - org_number in companies list — Bolagsverket public data; the swarm's "could be enskild firma personnummer" framing isn't accurate (enskild firma org_number IS the personnummer, but it's already in the public Bolagsverket business register). - Health endpoint information disclosure — intentional for a public developer API; matches Stripe/Twilio convention. - llms.txt / skills index MCP URL disclosure — that's the file's purpose. - Cache-Control public on discovery routes — content is by definition public; getCanonicalBaseUrl() removes the previous spoof concern. - Duplicate-email screen — user's own input; out of scope for this PR. 50/50 v1 tests pass; @supabase/supabase-js#createClient mocked so the public-path tests don't need real env vars. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(test): widen validateApiKey result assertions to include mode field The core-only CI job failed on two pre-existing api-keys.test.ts assertions that used strict toEqual matching against the old (userId, companyId, scopes) shape. The wrapper migration in this PR widened that shape with mode, apiKeyId, and apiKeyName. Update both existing assertions to match the current shape and add a third test that exercises the mode='test' path. 3027/3027 vitest tests now pass locally. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): fourth-pass hardening — env guards, IP range check, headers on wrapped routes Address the fourth compliance-swarm sweep on PR #450: - ISO A.5.17 / SOC 2 CC6.1 (high): createAnonClient now fails closed with an explicit Error if NEXT_PUBLIC_SUPABASE_URL or _ANON_KEY are missing, surfacing misconfiguration on the first request instead of throwing deeper in the handler with no context. - GDPR Art.5(1)(f): truncateIp now rejects IPv4 with out-of-range octets (>255). '999.999.999.999' now returns undefined instead of a pseudo-IP that would pollute abuse-pattern analysis. Edge octets (0, 255) still accepted. 2 new tests. - OWASP V3.2 / V3.3: the wrapper's stampHeaders step now applies the full security header set to every wrapped v1 response (CSP, HSTS, X-Frame, Referrer-Policy, X-Content-Type-Options) PLUS X-Robots-Tag: noai, noimageai so authenticated payloads are excluded from AI training sets. Public discovery routes (llms.txt, skills index, openapi.json) deliberately omit X-Robots-Tag — being AI-discoverable is the whole point of those surfaces. - New WRAPPED_RESPONSE_HEADERS export separates the two contexts. Pushing back on: - SOC 2 CC6.1 medium "API key prefix in public docs aids brute force" — inverted logic. Every public API publishes its key prefix specifically so secret scanners (GitHub Advanced Security, GitLeaks) can detect leaks. Stripe (sk_live_), GitHub (ghp_), OpenAI (sk-) all do this. - SOC 2 CC6.3 medium "formal risk register for unsigned cursors" — org -level documentation, outside this PR. Code-comment already documents the trade-off. - SOC 2 CC2.3 low "llms.txt hardcodes security@arcim.io" — same address as SECURITY.md; no drift risk. Flagged separately (not changed): the register-page duplicate-email detection in this branch defeats Supabase's user-enumeration obfuscation (GDPR Art.5(1)(c) × 2, ISO A.8.11). Substantive product decision: UX (no infinite-wait for non-existent accounts) vs security (no enumeration). GitHub and Stripe Atlas pick UX; some pick security. Owner's call. 3029/3029 vitest tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(api): address Greptile review on PR #450 - P1 (companies/route.ts): keyset pagination was missing its tiebreaker. The cursor encoded (joined_at, id) but the filter only applied .gt('joined_at', ts) — same-joined_at rows on a page boundary could be skipped or duplicated. Also the encoded id was companies.id while the sort was on company_members, mismatched. Fixed: select + sort + encode on company_members.id, apply compound joined_at.gt.{ts} OR (joined_at.eq.{ts} AND id.gt.{cursor_id}) via .or(). Side benefit — eliminates the broken-cursor-on-null-join case (#2) because company_members.id is always present, no null guard needed. - P2 (registry.ts): ZodUnion branch had a dead ternary (['x','y','z','w'].length > 0 ? undefined : 'object') that always yielded undefined. Removed; emit { oneOf: [...] } without top-level type (correct JSON Schema for a union). - P2 (with-api-v1.ts): public-endpoint path was short-circuiting before Bearer-token validation, contradicting the JSDoc and PR description. Now opportunistically validates a supplied token for rate-limit attribution + key tracking; missing/invalid token silently falls back to anon (the route is public by definition, so we don't 401). Two new tests cover both branches. 3031/3031 vitest tests pass; build green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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 |
||
|
|
b387a77bfd |
chore: remove Sentry, consolidate migrations, add test coverage (#244)
* 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> |
||
|
|
cd376e1cad | feat: implement viewer role permissions for bank transaction imports and connections (#234) | ||
|
|
a1a816b4a5 |
Delete features (#218)
* 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. |
||
|
|
6d75b9a1bf |
feat: BankID authentication via TIC Identity API (#192)
* feat: add BankID authentication via TIC Identity API Integrate BankID as a login/signup method using the TIC Identity API. Users can authenticate with BankID QR codes (desktop) or deep links (mobile), link BankID to existing accounts, and skip TOTP MFA when BankID is linked. Removes Step 0 (role choice) from onboarding for all users. Adds enrichment data support for pre-filling company details from Bolagsverket during signup. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: address PR review — server-side rate limit, unlink clears MFA bypass - Add per-IP rate limit (5s cooldown) on /bankid/start to prevent unbounded billable TIC sessions from unauthenticated callers - Add /bankid/unlink endpoint that deletes bankid_identities AND clears app_metadata.bankid_linked so MFA enforcement resumes after unlink - Update BankIdSettings to call server-side unlink instead of client-side delete Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: move rate limiter to module scope, add BankID logo and year-end skill Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
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> |