diff --git a/DECISIONS.md b/DECISIONS.md index 46c23ebd..83dceb00 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -890,3 +890,4 @@ One line per decision: `[YYYY-MM-DD] : `. 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. diff --git a/extensions/general/arcim-migration/index.ts b/extensions/general/arcim-migration/index.ts index cfa63f91..3a953b4e 100644 --- a/extensions/general/arcim-migration/index.ts +++ b/extensions/general/arcim-migration/index.ts @@ -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) diff --git a/lib/providers/fortnox/__tests__/oauth.test.ts b/lib/providers/fortnox/__tests__/oauth.test.ts index 9f89467e..cb4acc51 100644 --- a/lib/providers/fortnox/__tests__/oauth.test.ts +++ b/lib/providers/fortnox/__tests__/oauth.test.ts @@ -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'); }); }); diff --git a/lib/providers/fortnox/oauth.ts b/lib/providers/fortnox/oauth.ts index 5d663375..bea5288d 100644 --- a/lib/providers/fortnox/oauth.ts +++ b/lib/providers/fortnox/oauth.ts @@ -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(