* fix(migration): resumable underlag import without inline extraction, same-origin MCP storage URLs
The Fortnox underlag import ran every file's AI extraction inline inside
one request and hit the hosted 300 s function limit after ~17 of 113 files
(twice on 2026-08-21); the UI showed the generic "underlagen kunde inte
importeras" although the files it did reach were linked. The import now
works in time-budgeted slices with a stable cursor (the UI loops until the
server reports the end and shows "x av y") and opts out of extraction
(extractionOwner 'none', stamped skipped:opted_out): every file is linked
to its posted verifikat on arrival, so the booking is already known.
MCP signed Storage URLs (upload_url, signed_url, download_url) are served
through a same-origin proxy, /api/storage/[...path], because Claude
Desktop's sandbox only reaches the MCP host and blocked the PUT to
<project>.supabase.co. The signed token stays the only credential; the
proxy forwards only signed documents-bucket paths to our own Storage host
and is a no-op rewrite when NEXT_PUBLIC_APP_URL is unset.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YoZ8iboyTj221axW6Gdtm
* fix(mcp): keep the storage-proxy note out of the size-capped tool descriptions
The per-tool 280-char cap and the tools/list payload ceiling both tripped on
the two sentences added to gnubok_create_document_upload and
gnubok_get_document_content; the why now lives in a code comment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YoZ8iboyTj221axW6Gdtm
* fix(review): id cursor, stall = error, capped upload body, encoded dot segments
Review follow-ups on #1783:
- the import cursor is the last handled provider attachment id, not an
index, so a file Fortnox adds or removes mid-sweep shifts nothing
- a partial answer whose cursor does not advance (or the round guard) is
reported as ARCIM_DOCUMENT_IMPORT_STALLED instead of "complete"; the
slices already landed stay reported and the retry button resumes
- the storage proxy reads the PUT body as a capped stream instead of
buffering an unbounded payload before measuring it
- object paths are rejected when any segment decodes to "." or ".." (or
holds a separator), and the URL fetch() would actually request is
re-checked against the allowlist after normalisation
- download_url description no longer claims a direct Storage URL
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013YoZ8iboyTj221axW6Gdtm
---------
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The Fortnox document import needs the archive and connectfile scopes, which the
registered Fortnox app does not have. Since #1549 pulled them out of the connect
request (they broke every connect with invalid_scope before login), every
attachment call fails and the user was told "Koppla om Fortnox och godkann
behorigheterna", under a button that reruns an authorize URL still not asking
for those scopes. Klura AB followed that loop four times and bought the Fortnox
Arkiv module trying to satisfy it. Prod evidence: no Fortnox attachment has ever
imported, across 166 companies and 24 consents since the feature shipped, and no
live token carries the scopes.
The error and the scope list now derive from one flag,
FORTNOX_DOCUMENT_SCOPES_APPROVED. While it is false a permission failure maps to
a new PROVIDER_DOCUMENT_SCOPES_UNAVAILABLE, which says the permission is missing
on our side, that reconnecting will not help, and that the rest of the migration
came through; the card offers no button, because no user action can succeed.
The attachment scopes also become an opt-in consent rather than part of every
connect. Fortnox derives customer licence requirements from what an integration
requests, so asking everyone for Arkivplats would put a licence in front of
customers who never import a receipt; and keeping it off the default connect
caps the blast radius of a wrong portal registration at the underlag flow rather
than every Fortnox connection. buildFortnoxAuthUrl already took per-call scopes,
provider-client simply never passed any, so this threads documentScopes from
that one button through /connect into the authorize URL.
A document consent is always a superset of an ordinary one: the callback
overwrites the consent's tokens in place, so a narrower grant would revoke the
migration's own ledger access. Pinned by a test that holds either way the flag
is set, alongside one for the 400-with-behorighet answer that six companies hit
between 08-13 and 08-19 and saw only a generic retry for.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Batch from a real migration walkthrough (Fortnox -> Accounted, 2026-08-20):
- Årsredovisning: the "Låst version" select was empty with no explanation
because the only version was a draft and "Lås version för underskrift"
is disabled while the four Lagstadgade upplysningar checkboxes and the
content confirmation count as blockers. The select is now disabled with a
hint that names the blocker count and links to Fullständighetskontroll,
the four AR-NOTE-*-UNCONFIRMED issues carry remediation text, the lock
button explains why it is grey, and "Markera som signerad" says what it
still needs (locked version, bevisreferens, date).
- Moderföretagets org.nr accepts a foreign registration identifier
(CHE-123.456.789, HRB 12345, 923 609 016); personnummer shapes stay out.
- Fortnox underlag discovery: log status, body and Fortnox's message on
failure, show the message in the UI, treat a 400 with behörighet/scope
text as scopes-required, and fall back to an unfiltered
voucherfileconnections list when the financialyear filter answers 400.
- Kontomapping: the Momskod column had min-w only; table-fixed collapsed it
and its selects overflowed into Konfidens. Real w-72 now.
- SIE import warnings pluralise correctly for one skipped voucher; the
Verifikationsserie option says the source series is preserved.
Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>