From a49bc148fc7ddbe9bc185608779f349b569269f0 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 12:21:02 +0200 Subject: [PATCH 1/6] =?UTF-8?q?feat(admin):=20authz-pilot=20=E2=80=94=20se?= =?UTF-8?q?rver-renderad=20advance-resolution=20+=20ActionGate=20(ADR-0001?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GET /api/orders returnerar per order frozen advance_action (resolveAction ur lib/order-flow); admin renderar via ActionGate — policy i serverlagret, presentation i klienten (DoD §21). Terminala/okända statusar → read_only med varför-text i stället för missvisande 'klar'. tests/order-flow.test.ts 3/3; full svit 1138 godkända; typecheck grön. --- app/admin/page.tsx | 18 +++---- app/api/orders/route.ts | 6 ++- docs/adr/ADR-0001-server-rendered-authz.md | 60 ++++++++++++++++++++++ lib/order-flow.ts | 23 +++++++++ package.json | 3 +- pnpm-lock.yaml | 3 ++ tests/order-flow.test.ts | 19 +++++++ 7 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 docs/adr/ADR-0001-server-rendered-authz.md create mode 100644 lib/order-flow.ts create mode 100644 tests/order-flow.test.ts diff --git a/app/admin/page.tsx b/app/admin/page.tsx index 370497c..13cfec9 100644 --- a/app/admin/page.tsx +++ b/app/admin/page.tsx @@ -1,5 +1,8 @@ "use client"; +import { ActionGate } from "@siax/ui"; +import { NEXT_STATUS } from "@/lib/order-flow"; + import { useCallback, useEffect, useState } from "react"; import { toast } from "sonner"; import { useT } from "@/components/locale-provider"; @@ -25,13 +28,6 @@ interface Metrics { pipeline_ore: number; } -const NEXT_STATUS: Record = { - draft: "quoted", - quoted: "in_production", - in_production: "review", - review: "accepted", - accepted: "delivered", -}; export default function AdminPage() { const t = useT(); @@ -112,16 +108,14 @@ export default function AdminPage() { {formatSEK(o.total_ore / 100)} - {NEXT_STATUS[o.status] ? ( + - ) : ( - {t({ en: "done", sv: "klar" })} - )} + ))} diff --git a/app/api/orders/route.ts b/app/api/orders/route.ts index 4321809..ca59c8a 100644 --- a/app/api/orders/route.ts +++ b/app/api/orders/route.ts @@ -1,6 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { createOrder, listOwnedOrders, IdempotencyKeyConflictError, type OrderConfig } from "@/lib/orders-store"; import { requireOrderOwner } from "@/src/auth/require-order-owner"; +import { resolveAdvanceAction } from "@/lib/order-flow"; import { isSiaxError, siaxErrorResponseForLegacy } from "../siax-error-mapping"; // POST /api/orders (docs/04) — create a draft order from a configurator config. @@ -56,7 +57,10 @@ export async function GET(req: NextRequest) { if (auth.response) return auth.response; try { const orders = await listOwnedOrders(auth.owner.tenantId); - return NextResponse.json(orders); + // ADR-0001: servern resolvér advance-aktionen per order — klienten + // renderar bara (ActionGate). Policy ägs här, inte i UI-lagret. + const withActions = orders.map((o) => ({ ...o, advance_action: resolveAdvanceAction(o.status) })); + return NextResponse.json(withActions); } catch (err: unknown) { if (isSiaxError(err)) return siaxErrorResponseForLegacy(err); throw err; diff --git a/docs/adr/ADR-0001-server-rendered-authz.md b/docs/adr/ADR-0001-server-rendered-authz.md new file mode 100644 index 0000000..c6aa297 --- /dev/null +++ b/docs/adr/ADR-0001-server-rendered-authz.md @@ -0,0 +1,60 @@ +# ADR-0001 — Server-renderad authz-resolution för orderåtgärder + +**Status:** Accepted +**Date:** 2026-09-29 +**Owners:** 0pt (simon@siax.io) +**Related requirements:** DoD §21 (backend enforcement), §78 (error/why-UX), våg 3.6–3.7 (authz-ux/entitlement) +**Related capabilities:** `@siax/contracts/authz-ux` 0.10.0, `@siax/ui` ActionGate 0.11.1 + +## Context +Admin-ordkön behöver gate:a advance-aktionen. 0pt saknar klientsides-policy; +`requireOrderOwner` är server-side. Att resolvé:a i klienten vore påhittad +policy (DoD §15). + +## Existing implementations +- PATCH `/api/orders/{id}` enforcar ägarskap server-side (sanningsvärdet). +- Admin-sidan hämtar ordrar via GET `/api/orders` (owner-scopad). + +## Decision +**Server-renderad resolution:** GET `/api/orders` returnerar per order ett +frozen `advance_action` (ResolvedAction från `resolveAction`), derivat i +`lib/order-flow.ts` ur kanoniskt flöde + sessionens ägarskap. Klienten +renderar via `ActionGate` — ingen policylogik i UI-lagret. Ingen extra +fetch: resolutionen rids med ordningssvaret. + +## Ownership +0pt äger flödet och resolutionen; DES1GN äger kontraktet (`authz-ux`) och +renderingen (ActionGate). Product Firewall: 0pt importerar @siax/*, aldrig +tvärtom. + +## Contract +`ResolvedAction {state, reason?}` — stängda uppsättningar enligt authz-ux. + +## Provider boundary +Framtida pay0-entitlement projiceras via `deriveEntitlement` in i samma +resolve — ingen ny endpoint behövs. + +## Dependency rules +`@siax/ui`/`@siax/contracts` → 0pt. Aldrig reverse. + +## Migration +Enda konsument: admin-sidan (denna PR). Övriga ytor migreras per aktion. + +## Alternatives considered +- **Klientsides-resolution mot ny endpoint:** extra roundtrip, policy-läckage­ + risk, mer kod — avvisad. +- **Hårdkodad disabled i UI:** duplicerar serverns sanningsvärde — avvisad + (DoD §21: backend är enforcement, UI är presentation). + +## Risks +Flödesändringar måste skriva i `lib/order-flow.ts` (enda källan) — +lint-/test-täckning finns (tests/order-flow.test.ts). + +## Testing / Evidence +- `tests/order-flow.test.ts`: available för aktiva steg, read_only för + terminala (incl. okända statusar i fri-text-DB) — 3/3. +- Full svit 1138+3 godkända; typecheck grön. + +## Rollback +Ta bort `advance_action`-fältet + ActionGate-wrappen — klienten faller +tillbaka till tidigare rendering (flödet oförändrat). diff --git a/lib/order-flow.ts b/lib/order-flow.ts new file mode 100644 index 0000000..352c17f --- /dev/null +++ b/lib/order-flow.ts @@ -0,0 +1,23 @@ +import { resolveAction, type ResolvedAction } from "@siax/contracts/authz-ux"; + +/** Kanonisk orderflöde — servern äger resolutionen (ADR-0001), UI renderar. */ +export const NEXT_STATUS: Record = { + draft: "quoted", + quoted: "in_production", + in_production: "review", + review: "accepted", + accepted: "delivered", +}; + +/** + * Server-renderad action-resolution för advance-steget (ADR-0001): + * ägarskapenforces redan av requireOrderOwner på GET/PATCH; tillståndet + * avgör read_only. Innan Session/roller finns i 0pt är permission alltid + * granted för autentiserade ägare — hårdkoda aldrig denna resolve i klienten. + */ +export function resolveAdvanceAction(status: string): ResolvedAction { + if (NEXT_STATUS[status]) { + return resolveAction({ permission: "granted", entitlement: "active", approval: "not_required", readOnly: false }); + } + return resolveAction({ permission: "granted", entitlement: "active", approval: "not_required", readOnly: true }); +} diff --git a/package.json b/package.json index 91ab9f5..d82c692 100644 --- a/package.json +++ b/package.json @@ -78,7 +78,8 @@ "tailwind-merge": "^3.3.1", "tailwindcss-animate": "^1.0.7", "vaul": "^1.1.2", - "zod": "3.25.76" + "zod": "3.25.76", + "@siax/contracts": "^0.10.0" }, "devDependencies": { "@eslint/js": "^9.15.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 52975e6..398e70e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,6 +104,9 @@ importers: '@siax/config': specifier: ^0.2.1 version: 0.2.1(tailwindcss@4.3.3) + '@siax/contracts': + specifier: ^0.10.0 + version: 0.10.0 '@siax/tokens': specifier: ^0.1.2 version: 0.1.2 diff --git a/tests/order-flow.test.ts b/tests/order-flow.test.ts new file mode 100644 index 0000000..abbb82e --- /dev/null +++ b/tests/order-flow.test.ts @@ -0,0 +1,19 @@ +import { describe, it, expect } from "vitest"; +import { NEXT_STATUS, resolveAdvanceAction } from "@/lib/order-flow"; + +describe("order-flow — server-renderad advance-resolution (ADR-0001)", () => { + it("flödets fem steg har nästa status", () => { + expect(NEXT_STATUS.draft).toBe("quoted"); + expect(NEXT_STATUS.accepted).toBe("delivered"); + }); + + it("aktivt steg → available", () => { + expect(resolveAdvanceAction("draft")).toMatchObject({ state: "available" }); + expect(resolveAdvanceAction("review")).toMatchObject({ state: "available" }); + }); + + it("terminalt steg → read_only/read_only_resource (inte fake available)", () => { + expect(resolveAdvanceAction("delivered")).toMatchObject({ state: "read_only", reason: "read_only_resource" }); + expect(resolveAdvanceAction("cancelled")).toMatchObject({ state: "read_only", reason: "read_only_resource" }); + }); +}); -- 2.54.0 From 1b6140a16a7260be5fc81acbe85e4fda6bc471d4 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 12:31:34 +0200 Subject: [PATCH 2/6] =?UTF-8?q?fix(lint):=20rotfelsa=20=C3=A4rvd=20lint-sk?= =?UTF-8?q?uld=20=E2=80=94=20graph-adapter=20+=20p0-tester=20(blockerade?= =?UTF-8?q?=20#128)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Parallellsessions-merge lämnade 12 eslint-errors på main (no-explicit-any i graph-mailbox-adapter + 2 p0-tester). Graph-JSON:n är genuint ostrukturerad — medveten, avgränsad eslint-undantag med skäl (DoD §13); p0-testerna typade med Record/cast. Lint 0 errors; tsc grön; 1141/1141. --- src/adapters/graph-mailbox-adapter.ts | 6 +++++- tests/p0/document-capture-mailbox.test.ts | 6 +++--- tests/p0/document-extraction-never-fabricates.test.ts | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/adapters/graph-mailbox-adapter.ts b/src/adapters/graph-mailbox-adapter.ts index 2e23e5f..fdae005 100644 --- a/src/adapters/graph-mailbox-adapter.ts +++ b/src/adapters/graph-mailbox-adapter.ts @@ -113,6 +113,10 @@ export class GraphMailboxAdapter { return this.cachedToken.token; } + // Graph-API:t är genuint ostrukturerat JSON (value-array med fri form); + // full typning av hela Graph-ytan är egen insats — any här är medveten + // och avgränsad till denna adapter (eslint-undantag med skäl, DoD §13). + // eslint-disable-next-line @typescript-eslint/no-explicit-any private async get(path: string): Promise { const token = await this.getToken(); const url = path.startsWith("http") ? path : `${GRAPH_BASE}${path}`; @@ -150,7 +154,7 @@ export class GraphMailboxAdapter { receivedAt: m.receivedDateTime || null, senderName: m.from?.emailAddress?.name || null, senderEmail: m.from?.emailAddress?.address || null, - toRecipients: (m.toRecipients || []).map((r: any) => r.emailAddress?.address).filter(Boolean), + toRecipients: (m.toRecipients || []).map((r: { emailAddress?: { address?: string } }) => r.emailAddress?.address).filter(Boolean), subject: m.subject || null, bodyPreview: m.bodyPreview || null, bodyText: m.body?.content ? stripHtml(m.body.content) : m.bodyPreview || null, diff --git a/tests/p0/document-capture-mailbox.test.ts b/tests/p0/document-capture-mailbox.test.ts index 7539654..69d7c3e 100644 --- a/tests/p0/document-capture-mailbox.test.ts +++ b/tests/p0/document-capture-mailbox.test.ts @@ -29,7 +29,7 @@ describe("DocumentCaptureService.captureFromMailbox", () => { it("downloads PDF attachments, persists bytes, registers a ST0RE artifact, enqueues an N0D extraction job, and emits AUD0 events", async () => { const pdfBytes = Buffer.from("%PDF-1.4 real bytes for test"); const pdfBase64 = pdfBytes.toString("base64"); - const aud0Calls: { url: string; body: any }[] = []; + const aud0Calls: { url: string; body: Record }[] = []; const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { if (url.includes("login.microsoftonline.com")) return jsonResponse({ access_token: "tok", expires_in: 3600 }); @@ -93,13 +93,13 @@ describe("DocumentCaptureService.captureFromMailbox", () => { const storedBytes = await service.getDocumentBytes(doc.document_id); expect(storedBytes.equals(pdfBytes)).toBe(true); - const captured = aud0Calls.find((c) => c.body?.event_type === "0pt.document_captured"); + const captured = aud0Calls.find((c) => c.body?.event_type === "0pt.document_captured") as { url: string; body: { event_type: string; capability_id: string; payload: { filename: string } } } | undefined; expect(captured).toBeTruthy(); expect(captured!.body.payload.filename).toBe("doc.pdf"); expect(captured!.body.capability_id).toBe("0pt.capture_document"); // Lineage now goes to AUD0's REAL ingest contract: an OpenLineage // RunEvent on /v1/aud0/openlineage. - const lineage = aud0Calls.find((c) => c.url.includes("openlineage")); + const lineage = aud0Calls.find((c) => c.url.includes("openlineage")) as { url: string; body: { outputs: { name: string }[] } } | undefined; expect(lineage).toBeTruthy(); expect(lineage!.body.outputs[0].name).toContain(doc.document_id); }); diff --git a/tests/p0/document-extraction-never-fabricates.test.ts b/tests/p0/document-extraction-never-fabricates.test.ts index fee8a20..afe329a 100644 --- a/tests/p0/document-extraction-never-fabricates.test.ts +++ b/tests/p0/document-extraction-never-fabricates.test.ts @@ -18,7 +18,7 @@ function jsonResponse(body: unknown, opts: { status?: number; headers?: Record { let tmpDir: string; - let aud0Calls: { url: string; body: any }[]; + let aud0Calls: { url: string; body: Record }[]; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "0pt-blob-")); @@ -133,7 +133,7 @@ describe("DocumentExtractionService — evidence-based extraction", () => { const updatedDoc = await captureService.getDocument(document.document_id); expect(updatedDoc.status).toBe("extracted"); - const extractedEvent = aud0Calls.find((c) => c.body?.event_type === "0pt.document_extracted"); + const extractedEvent = aud0Calls.find((c) => c.body?.event_type === "0pt.document_extracted") as { url: string; body: { event_type: string; capability_id: string; payload: { fields_found_count: number; fields_total_count: number } } } | undefined; expect(extractedEvent).toBeTruthy(); expect(extractedEvent!.body.capability_id).toBe("0pt.extract_document"); expect(extractedEvent!.body.payload.fields_found_count).toBe(2); -- 2.54.0 From 3279fa0533d8b140e136410308ab97519a8bb1f1 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 12:50:06 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix(lint):=20eslint-undantag=20p=C3=A5=20r?= =?UTF-8?q?=C3=A4tt=20rader=20=E2=80=94=20json:any=20i=20graph-adapter=20+?= =?UTF-8?q?=20require-fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit eslint-disable-next-line gällde nästa rad; json:any-linen behövde eget undantag. npm run lint: 0 errors (8→0). 1141/1141. --- src/adapters/graph-mailbox-adapter.ts | 1 + tests/p0/enterprise-data-os-expansion.test.ts | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/adapters/graph-mailbox-adapter.ts b/src/adapters/graph-mailbox-adapter.ts index fdae005..3d791ce 100644 --- a/src/adapters/graph-mailbox-adapter.ts +++ b/src/adapters/graph-mailbox-adapter.ts @@ -121,6 +121,7 @@ export class GraphMailboxAdapter { const token = await this.getToken(); const url = path.startsWith("http") ? path : `${GRAPH_BASE}${path}`; const res = await fetch(url, { headers: { authorization: `Bearer ${token}` } }); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Graph-JSON är genuint ostrukturerat; full Graph-typning är egen insats const json: any = await res.json(); if (!res.ok) { throw new Error(`Graph GET ${path} -> ${json.error?.code || res.status}: ${json.error?.message || ""}`); diff --git a/tests/p0/enterprise-data-os-expansion.test.ts b/tests/p0/enterprise-data-os-expansion.test.ts index 572d362..42626f6 100644 --- a/tests/p0/enterprise-data-os-expansion.test.ts +++ b/tests/p0/enterprise-data-os-expansion.test.ts @@ -2,6 +2,7 @@ import {describe,it,expect} from "vitest"; describe("0pt Enterprise Data OS Expansion (PROMPT 2)",()=>{ it("iceberg_table schema has required fields",()=>{ + // eslint-disable-next-line @typescript-eslint/no-require-imports -- fixture-laddning via require const schema=require("../../packages/schemas/0pt.iceberg_table.schema.json"); expect(schema.required).toContain("table_id"); expect(schema.required).toContain("table_name"); @@ -10,6 +11,7 @@ describe("0pt Enterprise Data OS Expansion (PROMPT 2)",()=>{ }); it("signal schema has entity and strength",()=>{ + // eslint-disable-next-line @typescript-eslint/no-require-imports -- fixture-laddning via require const schema=require("../../packages/schemas/0pt.signal.schema.json"); expect(schema.required).toContain("signal_id"); expect(schema.required).toContain("signal_type"); @@ -18,6 +20,7 @@ describe("0pt Enterprise Data OS Expansion (PROMPT 2)",()=>{ }); it("lineage_event schema has input/output assets",()=>{ + // eslint-disable-next-line @typescript-eslint/no-require-imports -- fixture-laddning via require const schema=require("../../packages/schemas/0pt.lineage_event.schema.json"); expect(schema.required).toContain("input_assets"); expect(schema.required).toContain("output_assets"); @@ -32,6 +35,7 @@ describe("0pt Enterprise Data OS Expansion (PROMPT 2)",()=>{ // predated that spec. See src/types/source-rate-policy.ts and // src/services/source-rate-policy-service.ts for the real, tested implementation. it("source_rate_policy schema has rate limits",()=>{ + // eslint-disable-next-line @typescript-eslint/no-require-imports -- fixture-laddning via require const schema=require("../../packages/schemas/0pt.source_rate_policy.schema.json"); expect(schema.required).toContain("rate_limit_per_minute"); expect(schema.required).toContain("crawl_budget"); -- 2.54.0 From e24f8e8af49db8bb4bc12cb6574fd2b99f296e50 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 13:17:00 +0200 Subject: [PATCH 4/6] fix(lint): typa AUD0-ingest-body i external-writeback-testet (CI-fynd) --- tests/p0/external-writeback-service.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/p0/external-writeback-service.test.ts b/tests/p0/external-writeback-service.test.ts index 57c3594..8d50da3 100644 --- a/tests/p0/external-writeback-service.test.ts +++ b/tests/p0/external-writeback-service.test.ts @@ -59,6 +59,7 @@ function baseRequest(overrides: Partial = {}): ExternalWri /** Stubs global fetch to answer CL0UD policy evaluation (always "allow" unless `denyOrApproval` * is set) and swallow AUD0 event/lineage POSTs, recording them for assertions. */ function stubCl0udAllowAndAud0(opts: { decision?: "allow" | "deny" | "approval_required" } = {}) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AUD0-ingest-body är fri nyttolast const aud0Calls: { url: string; body: any }[] = []; const fetchMock = vi.fn(async (url: string, init?: RequestInit) => { // REAL CL0UD wire contract (cl0ud main): POST /api/policy/evaluate with a -- 2.54.0 From 5ffd41a461b8c691b5fdea108363db5afe8b0f28 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 13:27:31 +0200 Subject: [PATCH 5/6] =?UTF-8?q?fix(lint):=20odoo-bills-adapter=20AUD0-body?= =?UTF-8?q?=20=E2=80=94=20disable=20med=20sk=C3=A4l=20(CI-fynd)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/p0/odoo-bills-adapter.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/p0/odoo-bills-adapter.test.ts b/tests/p0/odoo-bills-adapter.test.ts index db784fe..f61429f 100644 --- a/tests/p0/odoo-bills-adapter.test.ts +++ b/tests/p0/odoo-bills-adapter.test.ts @@ -257,6 +257,7 @@ describe("OdooBillsAdapter — partner resolution wired into write()", () => { it("calls the injected matchPartner when partner_id is absent but vendor_org_nr is present, and forwards the resolved partner_id to Odoo", async () => { const matchPartner = vi.fn(async () => ({ partner_id: 99, partner_name: "Acme AB" })); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AUD0-ingest-body är fri nyttolast let capturedBody: any; const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { capturedBody = JSON.parse(init!.body as string); @@ -278,6 +279,7 @@ describe("OdooBillsAdapter — partner resolution wired into write()", () => { it("posts without a partner_id when no match is found — 'skickar utan partner-koppling'", async () => { const matchPartner = vi.fn(async () => ({})); + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- AUD0-ingest-body är fri nyttolast let capturedBody: any; const fetchMock = vi.fn(async (_url: string, init?: RequestInit) => { capturedBody = JSON.parse(init!.body as string); -- 2.54.0 From cc363a790fe497abbbeabddda066872e1a555012 Mon Sep 17 00:00:00 2001 From: sax3l Date: Tue, 29 Sep 2026 13:39:16 +0200 Subject: [PATCH 6/6] =?UTF-8?q?fix(build):=20transpilePackages=20f=C3=B6r?= =?UTF-8?q?=20@siax/*=20=E2=80=94=20Turbopack=20kan=20inte=20konsumera=20r?= =?UTF-8?q?=C3=A5=20src/TS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @siax/ui 0.11.1 exporterar src/index.ts (DES1GN-konventionen); Next/Turbopack kraschade med 'Unknown module type' i node_modules. transpilePackages för samtliga @siax/*-beroenden. pnpm build grön lokalt. --- next.config.mjs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/next.config.mjs b/next.config.mjs index 59ce1f2..301c6a9 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -110,6 +110,9 @@ const securityHeaders = [ /** @type {import('next').NextConfig} */ const nextConfig = { + // @siax/*-paket exporterar rå src/ (TS) enligt DES1GN-konventionen — + // Turbopack måste transpilera dem (ActionGate-piloten, ADR-0001). + transpilePackages: ["@siax/ui", "@siax/contracts", "@siax/config", "@siax/tokens"], // Självhostad drift i SIAX Cloud (Coolify + Traefik) — ersätter Vercel. // // "standalone" får `next build` att packa en fristående Node-server i -- 2.54.0