ca12b1855e
* fix(connect): PR #1758 CodeRabbit follow-up: harden connector status, i18n the connector-mode strings, align docs - getConnectorConfig() rebuilds baseUrl as origin + path: userinfo, query and fragment are stripped (warn-logged without the raw value) so nothing secret-shaped pasted into GNUBOK_CONNECT_URL survives into the /api/connector/status echo or the derived proxy URLs (CWE-200) - /api/connector/status responds Cache-Control: no-store on both branches (key prefix + wiring layout out of shared browser caches, CWE-525) - CWE-319 thread verified as no-change: both connector-mode helpers derive from getConnectorConfig(), which fails closed on non-https - SkatteverketConnectPanel tooltips and BankSyncNowButton gate/upsell strings moved to messages/sv.json + messages/en.json keys - DECISIONS.md: MD037 fix on line 1146 (backtick the glob), line 1147 reworded to grants-written-wiring-pending, decision lines appended (incl. declining the UpgradeNote children-append suggestion) - docs/SOVEREIGN.md availability wording aligned with SELF-HOSTING.md: infra merged, keys issued manually on request, client wiring pending Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKZUp1nePr8sVDoLkMSxbS * docs(connect): skeptic follow-up: bank client wiring is merged (#2094), SKV pending, no keys issued until it lands Skeptic refutation on PR #2098: SOVEREIGN.md claimed the services 'do not carry traffic' while this branch already contains #2094 (EB client proxy routing), and 'issued manually on request' contradicted the standing no-key-before-full-PR6b rule while skatteverketConnectorMode() has no client consumer yet. SOVEREIGN.md, SELF-HOSTING.md and DECISIONS.md line 1147 now all say: bank client wiring merged and carries traffic with a key, Skatteverket client wiring ships in a following release, keys are not issued until it lands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UKZUp1nePr8sVDoLkMSxbS --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
66 lines
2.5 KiB
TypeScript
66 lines
2.5 KiB
TypeScript
import { DEFAULT_CONNECT_BASE_URL } from '../contract'
|
|
import { createLogger } from '@/lib/logger'
|
|
|
|
const log = createLogger('connect/config')
|
|
|
|
/**
|
|
* Instance-side connector configuration (a self-hosted deployment).
|
|
*
|
|
* GNUBOK_CONNECTOR_KEY the `gnubok_ck_...` key issued for this instance
|
|
* GNUBOK_CONNECT_URL hosted origin, default https://app.gnubok.se
|
|
*
|
|
* Unset on hosted and on a self-host without a subscription: then the
|
|
* connector sync is a no-op and the connector capabilities stay gated.
|
|
*
|
|
* GNUBOK_CONNECT_URL must be https: the hourly sync sends the long-lived
|
|
* connector key as a Bearer header to this origin, so an http:// typo would
|
|
* ship the credential in plaintext. Plain http is allowed only for loopback
|
|
* hosts (local development against a dev server). An invalid or non-https
|
|
* URL disables the connector entirely (fail closed, nothing is sent).
|
|
*
|
|
* The returned baseUrl is rebuilt as origin + path: userinfo, query and
|
|
* fragment are stripped. They have no meaning in a base URL that gets paths
|
|
* appended to it, and /api/connector/status echoes baseUrl back to the
|
|
* operator, so anything secret-shaped pasted into the URL must not survive.
|
|
*/
|
|
export interface ConnectorConfig {
|
|
key: string
|
|
baseUrl: string
|
|
}
|
|
|
|
const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]'])
|
|
|
|
export function getConnectorConfig(): ConnectorConfig | null {
|
|
const key = process.env.GNUBOK_CONNECTOR_KEY?.trim()
|
|
if (!key) return null
|
|
const raw = (process.env.GNUBOK_CONNECT_URL?.trim() || DEFAULT_CONNECT_BASE_URL).replace(/\/+$/, '')
|
|
let url: URL
|
|
try {
|
|
url = new URL(raw)
|
|
} catch {
|
|
log.warn('GNUBOK_CONNECT_URL is not a valid URL; connector disabled', { value: raw })
|
|
return null
|
|
}
|
|
const loopback = LOOPBACK_HOSTS.has(url.hostname) || LOOPBACK_HOSTS.has(`[${url.hostname}]`)
|
|
if (url.protocol !== 'https:' && !(url.protocol === 'http:' && loopback)) {
|
|
log.warn('GNUBOK_CONNECT_URL must be https (http only for loopback); connector disabled', {
|
|
protocol: url.protocol,
|
|
host: url.hostname,
|
|
})
|
|
return null
|
|
}
|
|
const baseUrl = `${url.origin}${url.pathname.replace(/\/+$/, '')}`
|
|
if (baseUrl !== raw) {
|
|
// Log only the surviving value: the dropped parts are exactly what an
|
|
// operator might have pasted a credential into.
|
|
log.warn('GNUBOK_CONNECT_URL normalized to origin + path (userinfo/query/fragment stripped)', {
|
|
baseUrl,
|
|
})
|
|
}
|
|
return { key, baseUrl }
|
|
}
|
|
|
|
export function isConnectorConfigured(): boolean {
|
|
return getConnectorConfig() !== null
|
|
}
|