fix(providers): stop requesting unapproved Fortnox scopes that broke every connect (#1549)

PR #1541 added archive and connectfile to the Fortnox DEFAULT_SCOPES for
the voucher attachment import, but the registered Fortnox app does not
have those scopes approved in the Fortnox Developer Portal. Fortnox
rejects the authorize request with invalid_scope before login, which
broke every Fortnox connect in production within minutes of the deploy
(verified in Vercel runtime logs).

Remove the two scopes from the connect request; the attachment import
logic from #1541 stays fully intact and already degrades gracefully:
a 403 becomes PROVIDER_DOCUMENT_SCOPES_REQUIRED with a reconnect
follow-up card. Re-add the scopes once the portal registration has them
approved.

Also add charset=utf-8 to the OAuth callback HTML responses: without it
browsers render the Swedish error text as Latin-1 mojibake.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mattsson
2026-08-13 01:40:20 +02:00
committed by GitHub
co-authored by Claude Fable 5
parent b9bf60234d
commit 98612fb0ac
4 changed files with 23 additions and 7 deletions
+1
View File
@@ -890,3 +890,4 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
[2026-08-12] Automatic logout is opt-in per user (user_preferences.auto_logout, default OFF), reversing the 2026-07 always-on session timeouts: founder decision after a user complaint about multiple daily re-logins. The objection that an opt-in security control is effectively a removed one was raised and overruled; the compromise levers kept are NEXT_PUBLIC_SESSION_TIMEOUT_FORCE_ALL=true (re-enables enforcement for everyone without a code change) and the opt-in snapshot living inside the signed timeout cookie (no per-request DB read; the preference is read only at mint, and the preferences API clears the cookie on change so a toggle takes effect on the next request). No backstop absolute cap was added: "off" means the Supabase refresh-token lifetime governs, exactly the pre-hardening behavior. Pre-toggle cookies (no autoLogout field) are authentic-but-stale and re-minted preserving their timers, never routed down the tamper path, so the rollout logs nobody out.
[2026-08-12] Content dedupe on document ingest is an opt-in uploadDocument flag wired into the intake funnel (uploadAndExtract + mail-hunt ingest), NOT a unique index on (company_id, sha256_hash): archival callers (sent invoices, filings, bank exports) legitimately store repeating bytes and a blanket constraint would break them; the SELECT-then-insert race is accepted exactly as in the WhatsApp precedent. On a hit the funnel adopts the existing inbox item (callers always get a real inbox_item_id) or files an item against the existing document; the mail hunt skips outright since a second item would only duplicate work in Underlag.
[2026-08-12] Inbox "booked" state is derived server-side from the matched transaction (GET /items enrichment) in ADDITION to write-side created_journal_entry_id stamps, not stamps alone: created_journal_entry_id is UNIQUE (migration 20260515090000), so on a bulk-book samlingsverifikat only one of N matched items can ever carry the stamp; a stamp-only fix could not clear the reported flood of matched items on bulk-booked transactions. The constraint stays (it guards the book-direct double-fire race); the stamp becomes a fast path and the derivation the source of truth.
[2026-08-13] Pulled archive/connectfile out of Fortnox DEFAULT_SCOPES (reverting the #1541 addition) instead of enabling them in the Fortnox Developer Portal: requesting a scope the registered app lacks makes Fortnox reject authorize with invalid_scope before login, which broke EVERY Fortnox connect in prod within minutes of the #1541 deploy (verified in Vercel logs, user willemduplessis999/TETTET). The attachment-import feature itself stays: it already degrades via 403 -> PROVIDER_DOCUMENT_SCOPES_REQUIRED with a reconnect follow-up. Re-add the scopes only after the portal registration has them approved.
+6 -2
View File
@@ -541,7 +541,9 @@ export const arcimMigrationExtension: Extension = {
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html' },
// charset is required: without it browsers default to Latin-1 and
// render the Swedish text as mojibake (the "rätt behörigheter" bug).
headers: { 'Content-Type': 'text/html; charset=utf-8' },
})
}
@@ -622,7 +624,9 @@ export const arcimMigrationExtension: Extension = {
return new Response(html, {
status: 200,
headers: { 'Content-Type': 'text/html' },
// charset is required: without it browsers default to Latin-1 and
// render the Swedish text as mojibake (the "rätt behörigheter" bug).
headers: { 'Content-Type': 'text/html; charset=utf-8' },
})
} catch (error) {
log.error('OAuth callback exchange failed', error)
@@ -3,7 +3,12 @@ import { describe, expect, it } from 'vitest';
import { buildFortnoxAuthUrl } from '../oauth';
describe('Fortnox OAuth scopes', () => {
it('requests archive and file-connection access by default', () => {
// Pins the 2026-08-13 incident fix: the registered Fortnox app does not
// have the archive/connectfile scopes approved, and requesting a scope the
// app lacks makes Fortnox reject the authorize request with invalid_scope
// before the user can even log in. Do not add them back here until the
// Fortnox Developer Portal registration includes them.
it('does not request archive or connectfile until the Fortnox app has them approved', () => {
const url = new URL(
buildFortnoxAuthUrl({
clientId: 'client-id',
@@ -14,7 +19,7 @@ describe('Fortnox OAuth scopes', () => {
const scopes = new Set(url.searchParams.get('scope')?.split(' ') ?? []);
expect(scopes).toContain('bookkeeping');
expect(scopes).toContain('archive');
expect(scopes).toContain('connectfile');
expect(scopes).not.toContain('archive');
expect(scopes).not.toContain('connectfile');
});
});
+8 -2
View File
@@ -13,8 +13,14 @@ const DEFAULT_SCOPES = [
'customer',
'supplier',
'bookkeeping',
'archive',
'connectfile',
// 'archive' and 'connectfile' (voucher attachment import) must NOT be
// requested until the registered Fortnox app has them approved in the
// Fortnox Developer Portal: 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). The document import
// detects the missing scopes at runtime (403 becomes
// PROVIDER_DOCUMENT_SCOPES_REQUIRED) and surfaces a reconnect follow-up
// instead of failing the migration.
];
export function buildFortnoxAuthUrl(