* feat(connect): bank sync through the connector operation, with a per-company canary In connector mode the enable-banking sync no longer pages Enable Banking on the instance: it calls POST /api/connect/bank/sync on the hosted service with the session id it holds and the account, and receives booked, normalized rows plus the raw provider pages to archive. Everything downstream is shared with the direct path (stored external ids computed here from booking_date, amount and the account scope; ingest; archive; balance refresh), so a company that moves to the connector produces byte-identical keys. A 410 from the service maps onto the same SessionExpiredError the direct path throws. bankConnectorMode(companyId) gains CONNECT_BANK_CANARY_COMPANIES: listed companies use the connector even while the installation has its own Enable Banking credentials, which is how hosted Accounted moves its bank sync to Connect a few companies at a time before dropping its keys. The contract package gains the bank sync request/response schemas (2026-09-03). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> * fix(connect): calendar-valid dates, body read inside the timeout, service origin as the default Review follow-ups on #2205. The contract validates date_from, date_to and booking_date with z.iso.date() (2026-02-30 and an empty booking date are refused; the installation derives its stored keys from booking_date). The connector sync reads the response body inside the timeout window so a service that stalls the body cannot hold the sync open. DEFAULT_CONNECT_BASE_URL now names the connector service (connect.accounted.se), which is where the sync operation exists; the hosted app's copy of the connector routes is legacy and hosted Accounted itself sets GNUBOK_CONNECT_URL explicitly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> --------- 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>
75 lines
3.1 KiB
TypeScript
75 lines
3.1 KiB
TypeScript
import { getConnectorConfig } from './config'
|
|
import {
|
|
hasOwnEnableBankingCredentials,
|
|
hasOwnPeppolCredentials,
|
|
hasOwnSkatteverketCredentials,
|
|
} from '@/lib/entitlements/own-credentials'
|
|
|
|
/**
|
|
* Instance-side connector routing for the bank and Skatteverket upstreams.
|
|
*
|
|
* An upstream is in "connector mode" when this instance has a connector key
|
|
* AND no own credentials for that upstream. Hosted always has its own
|
|
* credentials, so hosted is never in connector mode: the check is what keeps
|
|
* hosted byte-identical. A self-host that pastes GNUBOK_CONNECTOR_KEY and
|
|
* leaves ENABLE_BANKING_PRIVATE_KEY / SKATTEVERKET_OAUTH2_CLIENT_ID unset gets
|
|
* routed through the hosted proxy instead.
|
|
*
|
|
* The own-credentials checks live in lib/entitlements/own-credentials.ts:
|
|
* they were forward-ported into the entitlement partition (PR #1747) so the
|
|
* gate and this routing seam can never disagree about what "own credentials"
|
|
* means. Re-exported here for the instance-side callers.
|
|
*/
|
|
|
|
export const CONNECTOR_COMPANY_HEADER = 'X-Connector-Company'
|
|
export const CONNECTOR_UPSTREAM_AUTH_HEADER = 'X-Connector-Upstream-Authorization'
|
|
export const CONNECTOR_UPSTREAM_CONTENT_TYPE_HEADER = 'X-Connector-Upstream-Content-Type'
|
|
|
|
export { hasOwnEnableBankingCredentials, hasOwnPeppolCredentials, hasOwnSkatteverketCredentials }
|
|
|
|
export interface ConnectorUpstream {
|
|
/** Base URL to send upstream requests to (the hosted proxy). */
|
|
baseUrl: string
|
|
/** The connector key, sent as Authorization: Bearer for proxy auth. */
|
|
key: string
|
|
}
|
|
|
|
/**
|
|
* Company ids that use the connector for bank sync even though this
|
|
* installation has its own Enable Banking credentials: the canary switch for
|
|
* moving an installation upstream by upstream (hosted Accounted moves its
|
|
* bank sync to Connect a few companies at a time before dropping its own
|
|
* keys). Comma-separated. Ignored without a connector key.
|
|
*/
|
|
function bankCanaryCompanies(): Set<string> {
|
|
const raw = process.env.CONNECT_BANK_CANARY_COMPANIES?.trim()
|
|
if (!raw) return new Set()
|
|
return new Set(raw.split(',').map((v) => v.trim()).filter(Boolean))
|
|
}
|
|
|
|
export function bankConnectorMode(companyId?: string): ConnectorUpstream | null {
|
|
const cfg = getConnectorConfig()
|
|
if (!cfg) return null
|
|
if (hasOwnEnableBankingCredentials() && !(companyId && bankCanaryCompanies().has(companyId))) return null
|
|
return { baseUrl: `${cfg.baseUrl}/api/connect/bank`, key: cfg.key }
|
|
}
|
|
|
|
export function skatteverketConnectorMode(): ConnectorUpstream | null {
|
|
if (hasOwnSkatteverketCredentials()) return null
|
|
const cfg = getConnectorConfig()
|
|
if (!cfg) return null
|
|
return { baseUrl: `${cfg.baseUrl}/api/connect/skv`, key: cfg.key }
|
|
}
|
|
|
|
/**
|
|
* Peppol through Arcim's contracted access point. Same rule as the other
|
|
* upstreams: an instance with its own Qvalia partner keys runs Peppol itself
|
|
* and is never routed here.
|
|
*/
|
|
export function peppolConnectorMode(): ConnectorUpstream | null {
|
|
if (hasOwnPeppolCredentials()) return null
|
|
const cfg = getConnectorConfig()
|
|
if (!cfg) return null
|
|
return { baseUrl: `${cfg.baseUrl}/api/connect/peppol`, key: cfg.key }
|
|
}
|