feat(connect): Peppol through the connector (hosted proxy, instance transport, ownership ledger) (#2177)
* feat(connect): peppol connector foundation: capability, ledger/budget service, quota Adds the storage + package shape for brokering Peppol through the connector with the same one-address + rate-budget model as bank/skatteverket: a peppol capability (connector-gated, free on hosted), peppol as a ledger + upstream service, a conservative rate budget, and a migration extending the ledger service CHECK and the per-key limits (peppol_connections_per_company). Proxy route + instance-side Qvalia reroute follow. Switch-on gated on the Qvalia brokering-terms check. (cherry picked from commit 3cc0da6a3, migration renumbered 20260902190000) Signed-off-by: Jakob Wennberg <jakob.wennberg@arcim.io> Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMUUom4fUk6zi4xYZSSfat * feat(connect): Peppol through the connector: hosted proxy, instance transport, ownership ledger Completes the Peppol upstream for self-hosted instances on the connector (WS3): an instance with a connector key carrying the peppol scope and no Qvalia keys of its own sends and receives e-invoices through Arcim's contracted access point, the same way bank and Skatteverket already route. Hosted: app/api/connect/peppol/[...path] speaks the PeppolTransport operations (lookup, submit, status, evidence, recipient PUT/DELETE, inbound list/xml) rather than proxying Qvalia paths, because the Qvalia account is shared by every hosted company and every instance: reads must be scoped to what the caller owns, and the inbound read endpoint is destructive for the whole account. Ownership: a receiving registration is a ledger row (service peppol, participant id in account_uids, sha256 in handle_hash so one key holds a participant at a time); outbound submissions land in the new connector_peppol_submissions table and gate status/evidence; inbound documents are served from the hosted archive filtered by the participants the key holds. Per-company quota (peppol_connections_per_company), the shared PEPPOL_RECEIVING_MAX_REGISTRATIONS cap, and the global peppol rate budget apply. Provider failures cross as CONNECTOR_UPSTREAM_ERROR with the adapter's retryable flag (422 or 502). Instance: lib/invoices/transports/connector.ts implements PeppolTransport over that API and registers itself in connector mode (key present, no QVALIA_* keys); getPeppolTransportAvailability() defaults to it when no provider is selected, so an instance needs no PEPPOL_TRANSPORT_PROVIDER. Webhooks are not brokered; the existing outbound status poll covers it. Hosted is byte-identical: it has its own keys, so connector mode is never on. Docs: SELF-HOSTING.md, SOVEREIGN.md, .env.example. Switch-on for third-party instances stays gated on the Qvalia brokering-terms check; without the scope every operation answers 403. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMUUom4fUk6zi4xYZSSfat Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * fix(connect): authorize Peppol participants per key, harden the proxy after review Review follow-ups on #2177. Authorization: a key may only register (and send as) participant identifiers Arcim recorded on the key at issuance (connector_keys.peppol_participants, migration 20260902191000) or the licensee's own org number, and a document may only be submitted as a sender the key has registered; X-Connector-Company stays an opaque per-company ref. Cap: the shared access-point cap now counts fresh pending reservations and is re-checked after this request's own reservation, so concurrent registrations cannot both pass. Inbound: both halves of the participant id are filtered in the archive query (over-fetched, then exact-pair checked), so foreign rows sharing an identifier cannot consume the limit. Delete: deregistration is a required transport capability, checked before the ledger row is revoked, and registration refuses an access point that cannot deregister. Instance transport: the hosted URL must be https (loopback http only, same rule as getConnectorConfig), and the response body is read inside the timeout window with body-read failures mapped to retryable transport errors. issue-connector-key.ts gains --peppol-participants and --peppol-connections-per-company. Declined: NOT VALID on the ledger CHECK (the table is empty until keys are issued; the validated scan is instant). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMUUom4fUk6zi4xYZSSfat Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * fix(connect): bind Peppol ownership to the instance company, query exact participant pairs Second review round on #2177. Ownership is now (key, company_ref), not key alone: a sender must be registered under the same company header, status and evidence reads look the submission up under the header company, DELETE and re-registration refuse a participant the key holds for another company, so one company on a multi-company instance cannot act on another company's registration through the shared key. The instance transport resolves the owning company from its own peppol_deliveries / peppol_registrations rows before status, evidence and deregistration calls (deps.companyFor, deps.companyForParticipant, wired in transports/index.ts). Inbound listing stays key-wide (the instance routes documents to its own companies by its own registrations). The archive query now runs one exact-pair query per scheme (scheme fixed, that scheme's identifiers), so neither foreign nor cross-pair rows can consume the limit. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMUUom4fUk6zi4xYZSSfat Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> --------- Signed-off-by: Jakob Wennberg <jakob.wennberg@arcim.io> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
Jakob Wennberg
parent
fcfa1ba974
commit
f31eeaa603
@@ -339,7 +339,7 @@ Set this when you have turned public signup off in GoTrue (`disable_signup`). Th
|
||||
|
||||
### Connector subscription (self-hosted instances)
|
||||
|
||||
Everything a self-hosted instance runs itself is free (AGPL). Four capabilities depend on services only Accounted operates and are therefore gated on a self-host: bank sync (our PSD2/AISP credentials), Skatteverket API submission and skattekonto sync (our API client registration), company lookup (TIC) and migration from Fortnox/Visma/Bokio/Björn Lundén (the migration gateway). A **connector key** unlocks them for every company on the instance; it is priced per active company at parity with hosted and will be issued manually by Accounted (self-serve later); no keys are issued until the instance-side client wiring described below is complete.
|
||||
Everything a self-hosted instance runs itself is free (AGPL). Five capabilities depend on services only Accounted operates and are therefore gated on a self-host: bank sync (our PSD2/AISP credentials), Skatteverket API submission and skattekonto sync (our API client registration), Peppol e-invoicing (our contracted access point), company lookup (TIC) and migration from Fortnox/Visma/Bokio/Björn Lundén (the migration gateway). A **connector key** unlocks them for every company on the instance; it is priced per active company at parity with hosted and will be issued manually by Accounted (self-serve later); no keys are issued until the instance-side client wiring described below is complete.
|
||||
|
||||
```bash
|
||||
GNUBOK_CONNECTOR_KEY=gnubok_ck_... # issued by Accounted, shown once
|
||||
@@ -354,6 +354,8 @@ curl -sf -H "Authorization: Bearer $CRON_SECRET" http://localhost:3000/api/conne
|
||||
|
||||
The **bank** and **Skatteverket** connector proxies are live (`app.gnubok.se/api/connect/bank/*` and `/api/connect/skv/*`): with `bank_sync` / `skatteverket` in your key's scopes, the instance connects a bank through Arcim's PSD2 credentials and files VAT/AGI + syncs skattekonto through Arcim's registered Skatteverket client, while all tokens (the bank session id, the SKV BankID tokens) stay encrypted in the instance's own database. Company lookup and migration through the connector ship in following releases. The instance-side client wiring is merged for both upstreams: in connector mode (key set, no own credentials for that upstream) bank sync and Skatteverket carry traffic through the hosted proxy. Keys are not yet issued: Accounted issues none until a staging end-to-end run confirms the full flow, so a key never unlocks a granted capability whose client cannot carry traffic. On the instance, Skatteverket still needs `SKATTEVERKET_ENABLED=true` and `SKATTEVERKET_TOKEN_ENCRYPTION_KEY` (the tokens are stored there, so the encryption key is the operator's).
|
||||
|
||||
**Peppol** through the connector works the same way once your key carries the `peppol` scope: leave every `QVALIA_*` variable and `PEPPOL_TRANSPORT_PROVIDER` unset, and the instance sends and receives e-invoices through Arcim's contracted access point (`app.gnubok.se/api/connect/peppol/*`). The hosted side enforces one receiving registration per company (`peppol_connections_per_company` on the key), a shared cap on registrations at the access point, and ownership: an instance can only poll status, fetch evidence and receive documents for registrations and submissions made through its own key. Delivery status arrives by polling (`/api/peppol/outbound/status/cron`), not by webhook. Which participant identifiers (organisation numbers, GLNs) a key may register and send as is recorded on the key when Arcim issues it; the licensee's own organisation number is always allowed, anything else is refused with `CONNECTOR_PEPPOL_PARTICIPANT_NOT_ALLOWED`. Setting `QVALIA_API_KEY` or `QVALIA_PARTNER_REG_NO` switches Peppol out of connector mode onto your own access-point account. Brokered Peppol registers your companies under Arcim's access point, so the `peppol` scope is issued only where Arcim's provider terms allow it.
|
||||
|
||||
With this release the self-host image also ships the `enable-banking` and `skatteverket` extensions in its preset: without a key (or own credentials) they show the connector upsell instead of being absent, and `GET /api/connector/status` shows the operator how each upstream would be routed.
|
||||
|
||||
#### Own credentials (no connector key)
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ Two honest framings up front:
|
||||
- **What you get is regulatory-risk elimination, not a legal verdict.** Hosted Accounted runs on Supabase and Vercel in AWS eu-north-1 (Stockholm) with AI inference on AWS Bedrock inside the EU; each of those providers operates under its own GDPR transfer mechanisms and contract terms (Data Privacy Framework participation and/or standard contractual clauses, documented in their DPAs), and whether that combination satisfies your policy is your assessment to make, not a conclusion this guide draws. What a self-host on Swedish providers removes is the *exposure*: no provider in the chain is subject to US extraterritorial law (the CLOUD Act), which is exactly the risk Sweden's national cloud policy of May 2026 names. That holds only for the chain you actually run: a sovereign deployment that keeps a US-dependent service such as Resend for outbound email has that one touchpoint left (section 6 lists them). The policy is principles for the public sector, not a mandate; it is still the document a procurement officer can point at.
|
||||
- **Not every Swedish accounting vendor runs on US clouds**, so do not buy this guide as a claim that "everyone else does". Buy it because you want to be able to prove, provider by provider, where your books are.
|
||||
|
||||
Everything here is free to run under the AGPL. Services that only Accounted can operate (bank sync through our PSD2 licence, Skatteverket API submission, company lookup, provider migration) are hosted-only today; a connector subscription for self-hosted instances is **not yet available**: the infrastructure is merged, but no keys are issued until the instance-side client wiring is complete (see "What is and is not covered" below). Manual filing of VAT and AGI declarations (file generation, you upload at Skatteverket) is always free and works on a self-host.
|
||||
Everything here is free to run under the AGPL. Services that only Accounted can operate (bank sync through our PSD2 licence, Skatteverket API submission, Peppol through our contracted access point, company lookup, provider migration) are hosted-only today; a connector subscription for self-hosted instances is **not yet available**: the infrastructure is merged, but no keys are issued until the instance-side client wiring is complete (see "What is and is not covered" below). Manual filing of VAT and AGI declarations (file generation, you upload at Skatteverket) is always free and works on a self-host.
|
||||
|
||||
## 1. What a sovereign deployment looks like
|
||||
|
||||
@@ -40,6 +40,7 @@ Three things carry the sovereign claim, in order of how much they matter:
|
||||
|---|---|
|
||||
| Double-entry bookkeeping, invoicing, supplier invoices, reports, SIE import/export | Bank sync via Enable Banking (runs on Accounted's PSD2/AISP credentials) |
|
||||
| VAT and AGI file generation for manual filing at Skatteverket | Skatteverket API submission and skattekonto sync (Accounted's API client registration) |
|
||||
| Peppol BIS Billing 3 invoice generation (UBL download) | Peppol sending and receiving through the network (Accounted's contracted access point; an own Qvalia account also works) |
|
||||
| Document archive with SHA-256 integrity and WORM bucket | Company lookup (TIC), migration from Fortnox/Visma/Bokio/Björn Lundén via the Arcim gateway |
|
||||
| MCP server, API keys, staged approvals | Receipt hunt in a connected mailbox (Accounted's Google OAuth app), WhatsApp intake (Accounted's Meta credentials), Stripe billing |
|
||||
| AI document extraction, assistant Q&A and one-tap categorization on a BYO endpoint; HTML mail invoices | Specialized conversational flows (VAT review, KPI explanation, settings help, bokslut helpers): Anthropic-family backend only (Bedrock or the direct API), not a BYO OpenAI-compatible endpoint ([#1800](https://github.com/erp-mafia/accounted/issues/1800)) |
|
||||
|
||||
Reference in New Issue
Block a user