diff --git a/DECISIONS.md b/DECISIONS.md index d30f940e..ab1c8017 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -1298,6 +1298,7 @@ One line per decision: `[YYYY-MM-DD] : `. Appended by agents and [2026-08-27] New `unlinked_documents` category on the Accounted://attention resource, backed by lib/documents/unlinked-documents.ts. The whole design is the mime ALLOW-LIST, and the naive predicate is a trap: "current version, no journal_entry_id, referenced by none of the eight linking tables" returns 15 806 rows on prod, of which 11 309 are application/json and every single one is named psd2-response__pN.json, the archived PSD2 bank-API responses the integration stores as evidence of each fetch. Those are unlinked BY DESIGN; surfacing them would hand an agent 11 309 items of work it must not action, which is worse than showing nothing. Measured 2026-08-27: application/json was 11 309 of 11 309 psd2, and pdf/png/jpeg/heic were 0 of 4 495, so the split is clean. Chose an allow-list of underlag-shaped mime types over excluding known-bad filenames, so a future machine-payload format (XML, CSV, an audit bundle) stays out by default instead of leaking until someone notices. Real remaining surface: 4 497 documents across 210 companies, median 3 per company, 481 in the preceding week, and NOT agent-specific (2 374 upload_source=api vs 1 623 file_upload from the web UI). Two-pass fetch mirroring fetchPurchasesWithoutUnderlag: indexed column filter, then eight reference lookups that run only when candidates exist, so the common case costs one query. Scan cap is 300 and is set by URL LENGTH, not table size: each candidate id is echoed through eight .in(column, ids) lookups at ~38 bytes per UUID, and a cap in the thousands would exceed the gateway limit, fail the lookups, and the "claims nothing" fallback would turn every candidate into a false positive. A failing lookup is deliberately treated as "claims nothing" (can only ADD a row) rather than dropping the category, so one misbehaving table cannot hide real work. UnlinkedDocument is a type alias not an interface: the resource assigns it into samples: Record[] and an interface has no implicit index signature; vitest does not typecheck so this only fails in npm run build. [2026-08-27] NOT fixed, and recorded so the next person does not act on an inflated number: the agent-facing readers (resources/attention.ts, resources/recent-activity.ts) still test booked-ness with a raw journal_entry_id null check instead of the canonical isTransactionBooked, which misses the bulk-book (transaction_voucher_links) and multi-allocation (invoice_payments / supplier_invoice_payments) cases. Real scale measured on prod 2026-08-27: 4 transactions, in 1 company, out of 567 column-filtered unbooked, all 4 via transaction_voucher_links and 0 via either payments table. Worth fixing as hygiene, but it is a 4-row problem and doing it properly in attention.ts needs the same two-pass treatment plus a decision about count semantics for a tenant with thousands of unbooked rows, so it does not belong bolted onto this change. [2026-08-27] Klarmarkera (markPeriodClosedExternally) gets an undo, reopenExternallyClosedPeriod, allowed only while the closed state still comes from klarmarkera (closed_externally set, no closing entry): that close was a person's control decision without a bokslutsverifikat, so reversing it strands nothing, whereas a closePeriod close keeps its closing entry and stays irreversible here. The reopen clears the lock too, because the reason to reopen is to change the period's contents (Forsslund Systems 2026-08-27: five imported years klarmarkerade, then the prior-year SIE turned out wrong; replace refused the closed year, unlock refused the closed state, no way back). Audit_log row plus period.unlocked event; the MCP staged-op surface (lock/unlock) does not get a reopen op yet, follow-up. +[2026-08-27] /mfa/enroll leave() always hard-navigates (window.location.assign) instead of router.push+router.refresh: enrolment raises the session to aal2 which middleware only re-evaluates on a document request, and the push/refresh pair raced leaving the user on the QR screen with 2FA already on (#1948); same pattern the file already used for /api/ destinations and /mfa/verify uses for its invite and route-handler paths. The E2E regression cover (spectest/tests/mfa.ts) lands with the spectest suite separately. [2026-08-27] Brand domains gate signup server-side (brands.signup_mode + brand_signup_allowlist), not in the register page: the browser used to call supabase.auth.signUp directly, so any client-side host check would be cosmetic. Email signup moved to POST /api/auth/signup on ALL hosts (byte-identical GoTrue call for open hosts); BankID gates in /bankid/complete; Google gates via the dashboard layout's brand-domain bounce (the account exists after OAuth, but gets no branded experience). Company invites bypass the allowlist because the invite is the authorization. Allowlisted signups' companies attach to the brand's byrå team via create_company_for_brand_signup (allowlist entry = the byrå's standing WL-15 authorization, recorded by an owner/admin); without the attach, WL-01 would home the company on the canonical domain, invisible on the very domain the user signed up on. Rejected a Supabase before-user-created hook: it does not reliably see the originating host and adds dashboard config coupling. [2026-08-27] New `tool-pg` vitest project: MCP tools driven through a REAL supabase-js client against a REAL PostgREST (tests/tool-pg/, scripts/tool-pg/reset.sh, `npm run tools:pg:reset` + `npm run test:tools`, plus a tool-pg CI job). NOT a duplicate of pg-real: that project holds a `pg` Pool and writes SQL, which structurally cannot see the half of a tool that PostgREST resolves at request time (the `.select()` column strings, the resource embeds, the `or=(...)` grammar, `.contains()` operand types). Before this, all 100 files in extensions/general/mcp-server/__tests__ faked supabase and query-journal.test.ts deferred its query chain to "the live MCP smoke test", which does not exist in CI: the PostgREST grammar of 157 tools was gated by nothing. Three findings worth keeping. (1) supabase-js hard-codes a `/rest/v1` prefix that a bare PostgREST does not serve, so the first version of the harness 404'd all 55 sweep queries, the tools reported the empty response as "Database error: undefined", and the suite passed GREEN while exercising nothing; fixed with a URL-rewriting `global.fetch` in createToolPgClient, and a permanent self-test now injects a bad column and asserts the harness detects 42703, so a green sweep means something. (2) Errors are captured at the TRANSPORT, not from the thrown Error: the tools wrap failures in their own prose and lose the payload, so a real 42703 arrives as an unclassifiable string. (3) The reset recreates the CONTAINER rather than dropping schemas: `storage` is owned by supabase_storage_admin so `DROP SCHEMA storage` fails as postgres, and dropping only `public` leaves the storage RLS policies migration 20240101000024 creates unconditionally, aborting the next replay partway and leaving a half-migrated database that looks like a migration bug. CI runs PostgREST via `docker run --network host` rather than a service container, because service containers on a non-containerized job are reachable from the runner but not from each other by name. Current coverage is honest and partial: 74 read tools, 87 real requests, 0 malformed queries, 4 failures all 22P02 from the empty argument set. Per-tool argument fixtures are what deepen it, and the harness is the thing that makes writing them worthwhile. [2026-08-27] Added `npm run check:types`, a typecheck ratchet (scripts/checks/no-new-type-errors.mjs + typecheck-baseline.json), wired into the core-build `checks` job next to check:lint. Reason: `npm test` does NOT typecheck. Vitest transpiles and discards types, so a type error passes all 18 000 tests and only surfaces in `npm run build` minutes later; that happened TWICE on 2026-08-27 (a widened errorKind union in the MCP server that lib/events/types.ts still contradicted, and an `interface` that would not assign into `Record[]` because interfaces have no implicit index signature). It is not merely a faster copy of the build job: `tsc --noEmit` also covers `__tests__` files, which the Next.js build never compiles, and that is where all 539 baseline errors live. Baseline is keyed per FILE, deliberately unlike the per-RULE lint ratchet: the legacy errors are concentrated in a handful of old test files and TS2322 is common enough that a code-keyed budget would silently absorb a real regression somewhere else, whereas per-file trips the moment a previously-clean file gains an error. Verified the gate actually fires by introducing a deliberate `const x: number = 'str'` and watching it fail with the exact location, then restoring. Cost measured: 36 s cold (what CI pays, since tsconfig.tsbuildinfo is gitignored) and 4.4 s warm locally via the existing `incremental: true`. The script sets NODE_OPTIONS=--max-old-space-size=8192 because a bare tsc dies with "Ineffective mark-compacts near heap limit" on this graph after about two minutes, which reads like a hang rather than a misconfiguration; it also detects that OOM string and exits 2 with a "raise HEAP_MB" message rather than silently reporting zero errors. NOT changed: Definition of Done item 1 still says only lint + test. CI enforcement is the stronger mechanism and does not need the policy edit; adding it to DoD is a founder call. diff --git a/app/(auth)/mfa/enroll/page.tsx b/app/(auth)/mfa/enroll/page.tsx index c7b8a038..07b012c2 100644 --- a/app/(auth)/mfa/enroll/page.tsx +++ b/app/(auth)/mfa/enroll/page.tsx @@ -37,16 +37,16 @@ function MfaEnrollContent() { const returnTo = safeReturnTo(searchParams.get('returnTo'), '/') + // Always a hard navigation, for two reasons that point the same way. // Route-handler destinations (the MCP OAuth consent page sends new - // password accounts here with returnTo=/api/mcp-oauth/authorize…) return - // raw HTML the client router cannot render: hard-navigate, like /mfa/verify. + // password accounts here with returnTo=/api/mcp-oauth/authorize...) return + // raw HTML the client router cannot render. And enrolling raises the + // session to aal2, which lib/supabase/middleware.ts only re-evaluates on a + // fresh document request: `router.push` followed by `router.refresh` raced, + // the refresh won, and the user was left on the QR screen with 2FA already + // active and no way forward but the address bar (#1948). const leave = () => { - if (returnTo.startsWith('/api/')) { - window.location.assign(returnTo) - return - } - router.push(returnTo) - router.refresh() + window.location.assign(returnTo) } // Back must not bounce into the consent page: with no factor enrolled it // redirects straight back here. Abort the connect flow to the app instead.