From f11a78ec501f6558f1c45ebd0bf3b7df59a8f6a5 Mon Sep 17 00:00:00 2001 From: Jakob Wennberg Date: Fri, 21 Aug 2026 09:20:28 +0200 Subject: [PATCH] feat(providers): request the Fortnox attachment scopes now that the portal grants them (#1776) Arkivplats and Koppla filer are enabled for integration 39254 in the Fortnox Developer Portal, so FORTNOX_DOCUMENT_SCOPES_APPROVED goes true and the opt-in underlag reconnect starts asking for archive and connectfile. The ordinary connect is untouched, so no customer is put in front of an Arkivplats licence to connect at all. This is the half that actually delivers the files. #1761 only stopped the loop that told people to grant a permission we never requested; until now no Fortnox token in production has ever carried these scopes, and no voucher attachment has ever imported (166 companies, 24 consents since the feature shipped). Set the flag back to false if the portal ever loses the scopes: authorize then rejects with invalid_scope before login, which now costs the underlag flow rather than every Fortnox connection. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) --- DECISIONS.md | 1 + lib/providers/fortnox/__tests__/oauth.test.ts | 14 ++++++++++++-- lib/providers/fortnox/oauth.ts | 15 +++++++++------ 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index c099a2a8..e2cf7584 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1136,3 +1136,4 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-19] #1674 audit fixed the in-repo disclosures only (privacy sub-processor Bedrock row now says AI requests go to Amazon Bedrock and the models are Anthropics Claude running inside Bedrock, replay paragraph states the deny-by-default guarantee, both locked by app/(public)/privacy/__tests__): Anthropic was not added as its own sub-processor row because the HOSTED posture is Bedrock by credential precedence (lib/ai/provider.ts: static AWS keys select Bedrock, region is AWS_REGION with eu-north-1 as the default, not a guarantee); a direct Anthropic API path DOES exist in code for self-hosted deployments (AI_PROVIDER=anthropic, or ANTHROPIC_API_KEY without static AWS keys), so "no code path sends data to Anthropic" would be false and the page asserts nothing about Anthropics underbitraede status either way; whether Anthropic is an underbitraede, plus aligning the published DPA PDF / marketing security page (which listed Anthropic/OpenAI in the US), is founder/legal action outside this repo. [2026-08-20] Reverted #1765: the company switcher is NOT mounted at the top of the desktop sidebar. Founder call after seeing it live: switching belongs in the bottom user block only (the UserMenu flyout), so the sidebar top stays brand + collapse and the nav starts immediately below. #1664's "one-click from the top" framing is therefore declined, not merely unimplemented; the logo title tooltip went back with the revert since it shipped inside the same commit. Do not re-add a top-of-sidebar switcher from #1664 without a new founder decision. [2026-08-20] Fortnox voucher-attachment scopes (Arkivplats + Koppla filer) are requested per authorize call from the underlag follow-up only, never from an ordinary connect, and gated on FORTNOX_DOCUMENT_SCOPES_APPROVED in lib/providers/fortnox/oauth.ts (the portal-registration switch). Two reasons: Fortnox derives customer licence requirements from what the integration requests, so an all-connects request would put an Arkivplats licence in front of customers who never import a receipt (the portal says so in as many words); and a scope the registered app lacks makes authorize reject with invalid_scope before login, so keeping it off the default connect caps the blast radius at the underlag flow instead of every Fortnox connection (incident 2026-08-13). A document consent is always a superset of an ordinary one, because the callback overwrites the consent's tokens in place and a narrower grant would revoke the migration's own ledger access. While the flag is false the attachment 403 reports PROVIDER_DOCUMENT_SCOPES_UNAVAILABLE with no action offered, instead of reconnect advice for a permission we never ask for: that advice sent Klura AB around the OAuth loop four times and to buy the Fortnox Arkiv module for nothing (support case 2026-08-20). Portal registration alone changes nothing observable, which is why turning the scopes on and back off that day neither caused nor fixed the error. +[2026-08-21] Flipped FORTNOX_DOCUMENT_SCOPES_APPROVED to true: Arkivplats and Koppla filer are now enabled for integration 39254 in the Fortnox Developer Portal (founder confirmed). Only the opt-in underlag reconnect requests them, so the ordinary connect is unchanged and no customer is asked for an Arkivplats licence to connect. Set it back to false if the portal ever loses the scopes, since authorize then rejects with invalid_scope before login. diff --git a/lib/providers/fortnox/__tests__/oauth.test.ts b/lib/providers/fortnox/__tests__/oauth.test.ts index ba58b49d..166d11e9 100644 --- a/lib/providers/fortnox/__tests__/oauth.test.ts +++ b/lib/providers/fortnox/__tests__/oauth.test.ts @@ -32,11 +32,21 @@ describe('Fortnox OAuth scopes', () => { // user is never told to reconnect for a permission the connect request does // not ask for. Flipping it here without enabling the scopes in the Fortnox // Developer Portal reintroduces the 2026-08-13 invalid_scope outage. - it('keeps the document scopes flagged as not approved, and names both of them', () => { - expect(FORTNOX_DOCUMENT_SCOPES_APPROVED).toBe(false); + it('has the document scopes approved in the portal, and names both of them', () => { + expect(FORTNOX_DOCUMENT_SCOPES_APPROVED).toBe(true); expect(FORTNOX_DOCUMENT_SCOPES).toEqual(['archive', 'connectfile']); }); + // The whole point of the opt-in consent: this is the only scope list that + // carries the attachment permissions, and the ordinary connect above still + // must not, so no customer is asked for an Arkivplats licence to connect. + it('puts the attachment scopes in the document consent only', () => { + expect(fortnoxConsentScopes({ documents: true })).toContain('archive'); + expect(fortnoxConsentScopes({ documents: true })).toContain('connectfile'); + expect(fortnoxConsentScopes()).not.toContain('archive'); + expect(fortnoxConsentScopes()).not.toContain('connectfile'); + }); + // Even once the portal registration lands, opting in must never cost the // consent its ledger access: the callback overwrites its tokens in place. it('keeps a document consent a superset of an ordinary one', () => { diff --git a/lib/providers/fortnox/oauth.ts b/lib/providers/fortnox/oauth.ts index f378ad6b..2bd90ffb 100644 --- a/lib/providers/fortnox/oauth.ts +++ b/lib/providers/fortnox/oauth.ts @@ -20,17 +20,20 @@ export const FORTNOX_DOCUMENT_SCOPES = ['archive', 'connectfile']; /** * Whether the registered Fortnox app has Arkivplats and Koppla filer enabled in - * the Fortnox Developer Portal (integration 39254). Requesting a scope the app - * lacks makes the authorize endpoint reject with invalid_scope BEFORE login, - * which kills every Fortnox connect (prod incident 2026-08-13), so this stays - * false until the portal registration has both. Flip it in the same change that - * enables them there. + * the Fortnox Developer Portal (integration 39254). True since 2026-08-21, when + * the portal registration was confirmed to carry both. + * + * Requesting a scope the app lacks makes the authorize endpoint reject with + * invalid_scope BEFORE login, so set this back to false the moment the portal + * loses them, rather than leaving the underlag reconnect pointed at a scope + * Fortnox will refuse (prod incident 2026-08-13, when the ordinary connect + * still carried these scopes and every Fortnox connection died). * * It gates the opt-in document consent below and the document-import error * message, never the ordinary connect: a user is never told to reconnect for a * permission we don't ask for (support case Klura AB, 2026-08-20). */ -export const FORTNOX_DOCUMENT_SCOPES_APPROVED: boolean = false; +export const FORTNOX_DOCUMENT_SCOPES_APPROVED: boolean = true; /** * The scopes a Fortnox consent is minted with. The document scopes are opt-in