#!/usr/bin/env bash # Read-only curl wrapper for the two banks Arcodange uses: Qonto + Wise. # # Usage: # bank-curl.sh qonto # e.g. bank-curl.sh qonto /v2/organization # bank-curl.sh wise # e.g. bank-curl.sh wise /v2/profiles # bank-curl.sh -i # include curl's -i (response headers) # # Reads credentials from ../../dolibarr/.env (the shared canonical file # used by every dolibarr-* and arcodange-* skill). Required vars: # QONTO_LOGIN, QONTO_SECRET_KEY (for qonto) # WISE_API_TOKEN (for wise) # # Exits non-zero on HTTP >=400 and writes the body to stdout + a short # "bank-curl.sh: HTTP " message to stderr — same shape as dol-curl.sh. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/../../dolibarr/.env" if [[ ! -f "${ENV_FILE}" ]]; then echo "bank-curl.sh: missing ${ENV_FILE}" >&2 echo " See dolibarr/README.md for the .env schema; arcodange-bank-reco extends it" >&2 echo " with QONTO_LOGIN, QONTO_SECRET_KEY, WISE_API_TOKEN, WISE_PROFILE_ID." >&2 exit 2 fi set -a; source "${ENV_FILE}"; set +a PASSTHRU=() while [[ $# -gt 2 ]]; do PASSTHRU+=("$1"); shift done if [[ $# -lt 2 ]]; then echo "bank-curl.sh: usage: bank-curl.sh [curl-opts] " >&2 exit 2 fi BANK="$1"; API_PATH="$2" case "${BANK}" in qonto) : "${QONTO_LOGIN:?bank-curl.sh: QONTO_LOGIN not set in .env}" : "${QONTO_SECRET_KEY:?bank-curl.sh: QONTO_SECRET_KEY not set in .env}" BASE="https://thirdparty.qonto.com" AUTH_HEADER="Authorization: ${QONTO_LOGIN}:${QONTO_SECRET_KEY}" ;; wise) : "${WISE_API_TOKEN:?bank-curl.sh: WISE_API_TOKEN not set in .env}" BASE="https://api.wise.com" AUTH_HEADER="Authorization: Bearer ${WISE_API_TOKEN}" ;; *) echo "bank-curl.sh: unknown bank '${BANK}' (use qonto or wise)" >&2 exit 2 ;; esac BODY_FILE="$(mktemp -t bankcurl.XXXXXX)" HEADERS_FILE="$(mktemp -t bankcurlhdr.XXXXXX)" trap 'rm -f "${BODY_FILE}" "${HEADERS_FILE}"' EXIT do_call() { local extra_header_1="${1:-}" extra_header_2="${2:-}" local extra_args=() [[ -n "${extra_header_1}" ]] && extra_args+=("-H" "${extra_header_1}") [[ -n "${extra_header_2}" ]] && extra_args+=("-H" "${extra_header_2}") curl -sS \ -H "${AUTH_HEADER}" \ -H "Accept: application/json" \ --max-time 30 \ -o "${BODY_FILE}" \ -D "${HEADERS_FILE}" \ -w "%{http_code}" \ ${PASSTHRU[@]+"${PASSTHRU[@]}"} \ ${extra_args[@]+"${extra_args[@]}"} \ "${BASE}${API_PATH}" } HTTP_CODE=$(do_call) # Wise SCA flow: a 403 with x-2fa-approval-result: REJECTED + x-2fa-approval: # header means the endpoint is sensitive and the call must be signed with our # registered RSA private key. We sign the one-time token and retry. if [[ "${BANK}" == "wise" && "${HTTP_CODE}" == "403" ]]; then ONE_TIME=$(awk 'tolower($1) == "x-2fa-approval:" { gsub(/[\r\n]/, "", $2); print $2; exit }' "${HEADERS_FILE}") if [[ -n "${ONE_TIME}" ]]; then KEY_PATH="${WISE_SCA_KEY_PATH:-}" # Expand ~ if used KEY_PATH="${KEY_PATH/#\~/$HOME}" if [[ -z "${KEY_PATH}" || ! -f "${KEY_PATH}" ]]; then echo "bank-curl.sh: Wise endpoint requires SCA but WISE_SCA_KEY_PATH is missing." >&2 echo " Generate a keypair, upload the public key to Wise, set WISE_SCA_KEY_PATH in .env." >&2 echo " See arcodange-bank-reco/SKILL.md for the setup steps." >&2 cat "${BODY_FILE}" exit 1 fi SIGNATURE=$(printf '%s' "${ONE_TIME}" | openssl dgst -sha256 -sign "${KEY_PATH}" | base64 | tr -d '\n') HTTP_CODE=$(do_call "x-2fa-approval: ${ONE_TIME}" "X-Signature: ${SIGNATURE}") fi fi cat "${BODY_FILE}" if [[ "${HTTP_CODE}" -ge 400 ]]; then echo "bank-curl.sh: HTTP ${HTTP_CODE} on ${BANK}${API_PATH}" >&2 exit 1 fi