1 Commits
Author SHA1 Message Date
Stephan D a5689876ff Connect local SXCHNGE to the Sendico stand 2026-10-16 10:19:59 +02:00
14 changed files with 335 additions and 8 deletions

No files matched your search

+7
View File
@@ -54,6 +54,9 @@ package-notifications-interface: generate-notifications
# Local product stack. Releases are resolved afresh, then pinned by digest. # Local product stack. Releases are resolved afresh, then pinned by digest.
COMPOSE = docker compose --project-directory . -f infra/local/compose.yml --env-file .local/dev/images.env COMPOSE = docker compose --project-directory . -f infra/local/compose.yml --env-file .local/dev/images.env
ifneq ($(wildcard .local/dev/sendico/settings.env),)
COMPOSE += -f infra/local/sendico/compose.yml
endif
.PHONY: build up down logs ps test-dev .PHONY: build up down logs ps test-dev
build: build:
sh infra/local/tools.sh build sh infra/local/tools.sh build
@@ -92,3 +95,7 @@ email:
test-dev-tooling: test-dev-tooling:
sh infra/local/tools.sh test sh infra/local/tools.sh test
.PHONY: sendico
sendico:
sh infra/local/tools.sh sendico
+2
View File
@@ -1,5 +1,7 @@
# Local development # Local development
For the connected Sendico stand, see [local Sendico integration](local-sendico.md).
Task [#42](https://git.sendico.io/tech/sxchnge/issues/42), Task [#42](https://git.sendico.io/tech/sxchnge/issues/42),
[PR #43](https://git.sendico.io/tech/sxchnge/pulls/43), branch `dev-env-42`. [PR #43](https://git.sendico.io/tech/sxchnge/pulls/43), branch `dev-env-42`.
+158
View File
@@ -0,0 +1,158 @@
# Local Sendico integration
Task [#52](https://git.sendico.io/tech/sxchnge/issues/52), branch `local-stand-52`,
based on `payments-50` / [PR #51](https://git.sendico.io/tech/sxchnge/pulls/51).
The local Sendico checkout uses `payments-2896` /
[PR #2897](https://git.sendico.io/tech/sendico/pulls/2897). Merge the prerequisites
before this infrastructure PR. No PR is merged by local startup.
## Running the configured stands
Start Sendico first from its checkout using its documented Make workflow. Its
Product API must respond at `http://localhost:8080/api/v1/health`. Then, here:
```sh
make build
make up
make test-dev
```
Open [SXCHNGE](https://localhost:5126),
[Sendico Product](http://localhost:3000), and
[Sendico Backoffice](http://localhost:4000).
SXCHNGE's Caddy CA is exported to `.local/dev/ca.pem`; trust that public CA in
the browser/OS if necessary. Bootstrap does not modify the system trust store.
The optional integration is selected by ignored `.local/dev/sendico/settings.env`.
Make and the containerized bootstrap both include
`infra/local/sendico/compose.yml` whenever that file exists. Standalone SXCHNGE
still uses six services. The connected stack adds one `sendico` session worker.
`make ps`, `make logs`, and `make down` include it automatically.
## Identity and configuration
The configured local stand has a dedicated `SXCHNGE Local` organization, active
integration account and account-bound confidential API client. The account is
an organization member with organization read/update, ledger read, payment
create/read, and recipient create/read/update permissions. Customer subjects
remain independent from that account. No pooled balances, customer funds or
payout destinations are seeded by this setup. Existing stand data is retained.
This identity was created through a one-time local administrative bootstrap;
runtime traffic and session creation use only the public HTTP API.
For another stand, an operator must first provision the organization, account,
client and grants in Sendico. Resolve actual IDs from that stand; do not copy
another environment's references. Initialize SXCHNGE's Vault using `make up`
before configuring the integration. Supply this JSON privately on stdin:
```json
{
"organization_ref": "<organization object ID>",
"source_account_ref": "<integration account object ID>",
"client_id": "<confidential client ID>",
"device_id": "sxchnge-local-bff",
"login": "<integration login>",
"password": "<integration password>",
"client_secret": "<confidential client secret>"
}
```
```sh
# Save operator input outside tracked files, with mode 0600. Never echo it.
make sendico < /private/path/sendico-input.json
make up
make test-dev
```
`make sendico` validates references and identifiers, stores only login/password/
client_secret at Vault `kv/services/bff/sendico`, and writes the non-secret client,
device, organization and source references into ignored local settings. The BFF
runtime is generated from `infra/local/bff/runtime.yml` with the existing
`api.sendico` configuration. Re-run configuration after changing that base YAML.
Remove the temporary input file once credentials have been stored in Vault.
No Sendico configuration parameters or application transport rules change.
## Connection and renewal
The session worker shares BFF's network namespace and binds a private loopback
TCP bridge at `127.0.0.1:18081`, forwarding to
`host.docker.internal:8080`. This uses the local stand's existing public HTTP API
port; it requires a local Docker engine with `host.docker.internal` support, as
provided by Docker Desktop. The bridge publishes no new host port. The adapter's
existing HTTPS requirement and loopback-only HTTP exception remain intact.
The worker authenticates to Vault with its own AppRole, reads exactly
`services/bff/sendico`, and revokes its five-minute Vault token after reading.
It cannot read Mongo credentials, JWT signing keys, NATS auth or administrator
material. Its initial login uses Sendico's public `/accounts/login/api` grant,
validates the access token's source account, full-session status and expiry, and
atomically publishes the token in a volume mounted read-only by BFF. Credentials
and response bodies stay in private tmpfs files and never appear in logs or
command arguments. Flutter receives no platform credentials.
Successful login repeats every 20 minutes before the one-hour access token
expires. A failure retains the prior token, logs one generic actionable message,
and retries after five minutes. The worker becomes unhealthy after 25 minutes
without renewal or when the upstream health endpoint fails; the adapter
separately rejects expired or mismatched tokens.
BFF waits for a recent token at startup. Following credential changes, restart
the worker with `make up` followed by the Compose restart command below, or wait
for its next renewal. Never change the integration account on an existing
financial environment without reviewing resource and idempotency bindings.
```sh
docker compose --project-directory . -f infra/local/compose.yml \
-f infra/local/sendico/compose.yml --env-file .local/dev/images.env restart sendico
```
`make test-dev` checks seven healthy services, verified browser TLS, the Vault
layout and least-privilege identities, Mongo transactions/lease rotation, and
authenticated Sendico currency and ledger reads through the same loopback
connection and token used by BFF. It sends no email and executes no payment.
An empty wallet is expected until dedicated subject-bound currency ledgers and
saved methods are provisioned as described in [payment provisioning](payments.md).
## Recovery
The existing stand had application processes stopped inside still-running Air
containers after a NATS disconnect and Mongo replica-state interruption. Restore
discovery first, then Treasury and the other service announcers. Wait until each
stage is registered before restarting dependent services: billing fees and
Ledger, then Limits/gateways/quotation/receiving routes, then payment orchestrator,
compliance assessment and Product BFF. The Sendico Makefile provides its ordered
backend startup workflow. Inspect current logs and health instead of assuming a
running development container has a live application process. Do not delete
volumes or reset financial data to recover service registration.
If an old Caddy leaf certificate expires while the stack is stopped, restart
frontend after startup so Caddy issues a fresh certificate. Preserve `caddy-data`
and the established CA. A subsequent verified TLS check must succeed.
## Acceptance and operator UX review
Validated on 2026-10-16: `make build`, `make up`, `make test-dev-tooling`,
`make test-dev-infra`, `make test-dev`, and `git diff --check` passed. Both product
sites, Sendico Backoffice and both API health endpoints returned HTTP 200;
SXCHNGE's browser endpoint was checked with its exported CA. All Sendico services
with health checks were healthy. Forced worker restart issued a different valid
same-account token and preserved successful authenticated currency/ledger reads.
The existing notification sender edit remains outside this task's commit.
The setup journey starts with the operator's Make invocation and completes when
Compose confirms service readiness; `make test-dev` additionally confirms the
authenticated integration. Existing command exit codes and Compose progress /
health states distinguish configuration, dependency, authentication and readiness
failures. A renewed-session log records credential recovery, while stale renewal
or upstream outage changes container health. Each new operator invocation is a
new attempt; worker retries remain within that process's lifetime. Command wall
time measures setup duration. Interrupted commands are incomplete attempts, not
successful launches. These are local operator signals, with no retained analytics
or cross-invocation conversion/abandonment reporting added.
The UX review covered missing bootstrap material, malformed operator input,
authentication failure, retained retry context, stale TLS recovery, successful
startup and confirmed API access. Commands reuse existing Make/Compose patterns,
errors give a recovery action, and success follows readiness. No secret values
appear in terminal feedback. Product layouts, focus and screen-reader behavior
are unchanged; browser interaction and funded payment settlement are not claimed
by HTTP readiness checks. No email or payment was sent during validation.
+1 -1
View File
@@ -1,3 +1,3 @@
ARG DOCKER_IMAGE ARG DOCKER_IMAGE
FROM ${DOCKER_IMAGE} FROM ${DOCKER_IMAGE}
RUN apk add --no-cache curl jq git openssl util-linux shellcheck RUN apk add --no-cache curl jq git openssl util-linux shellcheck socat
+10 -3
View File
@@ -54,24 +54,31 @@ provision_mongo() {
{ cat "$WORK/admin.js"; cat infra/local/mongo/mongo-client.js; } | mongo >/dev/null || fail 'Browser client provisioning failed' { cat "$WORK/admin.js"; cat infra/local/mongo/mongo-client.js; } | mongo >/dev/null || fail 'Browser client provisioning failed'
} }
provision_agents() { provision_agents() {
for service in bff notifications nats; do services='bff notifications nats'
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
for service in $services; do
directory="$STATE/agents/$service" directory="$STATE/agents/$service"
mkdir -p "$directory"; chmod 755 "$directory" mkdir -p "$directory"; chmod 755 "$directory"
case "$service" in case "$service" in
bff) paths='services/bff/jwt';; bff) paths='services/bff/jwt';;
notifications) paths='infrastructure/nats/auth services/notifications/email/resend';; notifications) paths='infrastructure/nats/auth services/notifications/email/resend';;
nats) paths=infrastructure/nats/auth;; nats) paths=infrastructure/nats/auth;;
sendico) paths=services/bff/sendico;;
esac esac
: > "$WORK/policy" : > "$WORK/policy"
for path in $paths; do printf 'path "kv/data/%s" { capabilities = ["read"] }\n' "$path" >> "$WORK/policy"; done for path in $paths; do printf 'path "kv/data/%s" { capabilities = ["read"] }\n' "$path" >> "$WORK/policy"; done
if [ "$service" != nats ]; then printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy"; fi case "$service" in bff|notifications) printf 'path "database/creds/%s" { capabilities = ["read"] }\n' "$service" >> "$WORK/policy";; esac
jq -n --rawfile policy "$WORK/policy" '{policy:$policy}' | vault "sys/policies/acl/$service" POST >/dev/null jq -n --rawfile policy "$WORK/policy" '{policy:$policy}' | vault "sys/policies/acl/$service" POST >/dev/null
if [ "$service" = sendico ]; then
printf '{"token_policies":["sendico"],"token_ttl":"5m","token_max_ttl":"5m","secret_id_ttl":"0","secret_id_num_uses":0}' | vault "auth/approle/role/$service" POST >/dev/null
else
jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null jq -n --arg service "$service" '{token_policies:[$service],token_period:"1h",secret_id_ttl:"0",secret_id_num_uses:0}' | vault "auth/approle/role/$service" POST >/dev/null
fi
vault "auth/approle/role/$service/role-id" | jq -r '.data.role_id' | write "$directory/role-id" 444 vault "auth/approle/role/$service/role-id" | jq -r '.data.role_id' | write "$directory/role-id" 444
if [ ! -f "$directory/secret-id" ]; then if [ ! -f "$directory/secret-id" ]; then
printf '{}' | vault "auth/approle/role/$service/secret-id" POST | jq -r '.data.secret_id' | write "$directory/secret-id" 444 printf '{}' | vault "auth/approle/role/$service/secret-id" POST | jq -r '.data.secret_id' | write "$directory/secret-id" 444
fi fi
if ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then if [ "$service" != sendico ] && ! cmp -s "infra/local/vault/$service.hcl" "$directory/config.hcl"; then
cat "infra/local/vault/$service.hcl" | write "$directory/config.hcl" 444 cat "infra/local/vault/$service.hcl" | write "$directory/config.hcl" 444
if compose ps --status running --services "$service" | grep -qx "$service"; then if compose ps --status running --services "$service" | grep -qx "$service"; then
compose up -d --no-deps --force-recreate --wait --wait-timeout 120 "$service" compose up -d --no-deps --force-recreate --wait --wait-timeout 120 "$service"
+7 -1
View File
@@ -14,7 +14,13 @@ trap cleanup EXIT
trap 'exit 130' INT trap 'exit 130' INT
trap 'exit 143' TERM trap 'exit 143' TERM
fail() { echo "$*" >&2; exit 1; } fail() { echo "$*" >&2; exit 1; }
compose() { docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"; } compose() {
if [ -f "$STATE/sendico/settings.env" ]; then
docker compose --project-directory . -f infra/local/compose.yml -f infra/local/sendico/compose.yml --env-file "$STATE/images.env" "$@"
else
docker compose --project-directory . -f infra/local/compose.yml --env-file "$STATE/images.env" "$@"
fi
}
write() { write() {
target=$1; mode=${2:-600} target=$1; mode=${2:-600}
mkdir -p "$(dirname "$target")" mkdir -p "$(dirname "$target")"
+1
View File
@@ -7,6 +7,7 @@ case "${1:-}" in
images) . infra/local/scripts/images.sh; images;; images) . infra/local/scripts/images.sh; images;;
up) . infra/local/scripts/bootstrap.sh; up;; up) . infra/local/scripts/bootstrap.sh; up;;
email) . infra/local/scripts/bootstrap.sh; email;; email) . infra/local/scripts/bootstrap.sh; email;;
sendico) . infra/local/scripts/bootstrap.sh; . infra/local/scripts/sendico.sh; configure_sendico;;
verify|verify-infra) . infra/local/scripts/verify.sh; existing; infrastructure; if [ "$1" = verify ]; then product; fi;; verify|verify-infra) . infra/local/scripts/verify.sh; existing; infrastructure; if [ "$1" = verify ]; then product; fi;;
rotation) . infra/local/scripts/verify.sh; . infra/local/scripts/rotation.sh; existing; rotation;; rotation) . infra/local/scripts/verify.sh; . infra/local/scripts/rotation.sh; existing; rotation;;
test) sh infra/local/scripts/test.sh;; test) sh infra/local/scripts/test.sh;;
+20
View File
@@ -0,0 +1,20 @@
# Operator input is JSON on stdin. Only credential values are stored in Vault.
configure_sendico() {
existing
cat > "$WORK/input"
jq -e '
(.organization_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
(.source_account_ref|test("^[0-9a-f]{24}$") and .!="000000000000000000000000") and
([.client_id,.device_id,.login,.password,.client_secret] | all(.[]; type=="string" and test("\\S"))) and
([.client_id,.device_id] | all(.[]; test("^[A-Za-z0-9._-]+$")))
' "$WORK/input" >/dev/null || fail 'Invalid Sendico operator input'
jq '{login,password,client_secret}' "$WORK/input" | put services/bff/sendico
mkdir -p "$STATE/sendico"
jq -r '"SENDICO_ORGANIZATION_REF="+.organization_ref+"\nSENDICO_SOURCE_ACCOUNT_REF="+.source_account_ref+"\nSENDICO_CLIENT_ID="+.client_id+"\nSENDICO_DEVICE_ID="+.device_id' "$WORK/input" | write "$STATE/sendico/settings.env"
{
cat infra/local/bff/runtime.yml
jq -r '"\n sendico:\n api_url: http://127.0.0.1:18081/api/v1\n organization_ref: "+.organization_ref+"\n source_account_ref: "+.source_account_ref+"\n token_file: /run/sendico/access-token"' "$WORK/input"
} | write "$STATE/sendico/runtime.yml" 644
provision_agents
echo 'Local Sendico connection configured. Run make up.'
}
+8
View File
@@ -4,4 +4,12 @@ umask 077
if [ "$SXCHNGE_SERVICE" = notifications ]; then if [ "$SXCHNGE_SERVICE" = notifications ]; then
exec service -config.file=/etc/sxchnge/notifications.yml exec service -config.file=/etc/sxchnge/notifications.yml
fi fi
if [ "${SXCHNGE_SENDICO_LOCAL:-}" = 1 ]; then
attempt=0
until [ -s /run/sendico/access-token ] && [ "$(($(date +%s) - $(stat -c %Y /run/sendico/access-token)))" -lt 1500 ]; do
attempt=$((attempt+1))
[ "$attempt" -lt 120 ] || { echo 'Local Sendico session did not become ready' >&2; exit 1; }
sleep 2
done
fi
exec service -config.file=/etc/sxchnge/bff.yml exec service -config.file=/etc/sxchnge/bff.yml
+21
View File
@@ -4,6 +4,7 @@ set -o pipefail
# Shell syntax and static analysis cover the actual container shell scripts. # Shell syntax and static analysis cover the actual container shell scripts.
shellcheck -s ash -x -S warning infra/local/scripts/dispatch.sh infra/local/scripts/test.sh shellcheck -s ash -x -S warning infra/local/scripts/dispatch.sh infra/local/scripts/test.sh
shellcheck -x -S warning infra/local/tools.sh infra/local/scripts/start-service.sh infra/local/mongo/start.sh infra/local/nats/start.sh infra/local/vault/start.sh shellcheck -x -S warning infra/local/tools.sh infra/local/scripts/start-service.sh infra/local/mongo/start.sh infra/local/nats/start.sh infra/local/vault/start.sh
shellcheck -s ash -x -S warning infra/local/sendico/session.sh infra/local/sendico/check.sh
. infra/local/scripts/common.sh . infra/local/scripts/common.sh
. infra/local/scripts/images.sh . infra/local/scripts/images.sh
. infra/local/scripts/bootstrap.sh . infra/local/scripts/bootstrap.sh
@@ -93,3 +94,23 @@ retire_secrets
retire_secrets retire_secrets
jq -e '.=={"infrastructure/nats/auth":true}' "$WORK/retirement" >/dev/null jq -e '.=={"infrastructure/nats/auth":true}' "$WORK/retirement" >/dev/null
echo 'PASS: obsolete caller secrets retired idempotently; NATS authentication preserved' echo 'PASS: obsolete caller secrets retired idempotently; NATS authentication preserved'
# Local integration configuration keeps credentials out of runtime settings and
# rejects malformed operator inputs before mutating Vault or service identities.
. infra/local/scripts/sendico.sh
STATE="$WORK/sendico-state"; mkdir "$STATE"
existing() { :; }
put() { cat > "$WORK/sendico-credentials"; }
provision_agents() { touch "$WORK/sendico-agents"; }
printf '{}' > "$WORK/sendico-invalid"
if (configure_sendico < "$WORK/sendico-invalid") >/dev/null 2>&1; then fail 'Invalid Sendico configuration accepted'; fi
[ ! -e "$WORK/sendico-credentials" ] || fail 'Invalid Sendico input mutated credentials'
cat > "$WORK/sendico-input" <<'JSON'
{"organization_ref":"000000000000000000000001","source_account_ref":"000000000000000000000002","client_id":"fixture.client","device_id":"fixture-device","login":"fixture@example.test","password":"fixture-password","client_secret":"fixture-secret"}
JSON
configure_sendico < "$WORK/sendico-input" >/dev/null
jq -e 'keys==["client_secret","login","password"]' "$WORK/sendico-credentials" >/dev/null
[ -f "$WORK/sendico-agents" ] || fail 'Sendico identity was not provisioned'
if grep -Eq 'fixture-secret|fixture-password|fixture@example' "$STATE/sendico/settings.env" "$STATE/sendico/runtime.yml"; then fail 'Credential leaked into runtime configuration'; fi
grep -q 'source_account_ref: 000000000000000000000002' "$STATE/sendico/runtime.yml"
grep -q 'api_url: http://127.0.0.1:18081/api/v1' "$STATE/sendico/runtime.yml"
echo 'PASS: Sendico operator input validation, secret separation and local runtime configuration'
+14 -3
View File
@@ -2,11 +2,16 @@ policies() (
root_token=$VAULT_TOKEN root_token=$VAULT_TOKEN
identity='' identity=''
trap 'if [ -n "$identity" ]; then printf "{}" | VAULT_TOKEN=$identity vault auth/token/revoke-self POST >/dev/null; fi' EXIT trap 'if [ -n "$identity" ]; then printf "{}" | VAULT_TOKEN=$identity vault auth/token/revoke-self POST >/dev/null; fi' EXIT
for service in bff notifications nats; do services='bff notifications nats'
if [ -f "$STATE/sendico/settings.env" ]; then services="$services sendico"; fi
for service in $services; do
directory="$STATE/agents/$service" directory="$STATE/agents/$service"
login=$(jq -n --rawfile role "$directory/role-id" --rawfile secret "$directory/secret-id" '{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' | vault auth/approle/login POST) login=$(jq -n --rawfile role "$directory/role-id" --rawfile secret "$directory/secret-id" '{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' | vault auth/approle/login POST)
identity=$(printf '%s' "$login" | jq -er '.auth.client_token') identity=$(printf '%s' "$login" | jq -er '.auth.client_token')
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","kv/data/infrastructure/mongo/bootstrap","sys/init","kv/data/bff","kv/data/caller","kv/data/services/notifications/callers/bff","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials' printf '%s' "$login" | jq '{token:.auth.client_token,paths:["kv/data/bootstrap/mongo","kv/data/infrastructure/mongo/bootstrap","sys/init","kv/data/bff","kv/data/caller","kv/data/services/notifications/callers/bff","kv/data/email","kv/data/nats","kv/data/tls"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Service can access bootstrap credentials'
if [ "$service" = sendico ]; then
printf '%s' "$login" | jq '{token:.auth.client_token,paths:["database/creds/bff","database/creds/notifications","kv/data/services/bff/jwt","kv/data/infrastructure/nats/auth"]}' | vault sys/capabilities POST | jq -e '.data | all(.[]; .==["deny"])' >/dev/null || fail 'Sendico session identity has unrelated secret access'
fi
VAULT_TOKEN=$(printf '%s' "$login" | jq -er '.auth.client_token') VAULT_TOKEN=$(printf '%s' "$login" | jq -er '.auth.client_token')
printf '{}' | vault auth/token/revoke-self POST >/dev/null printf '{}' | vault auth/token/revoke-self POST >/dev/null
VAULT_TOKEN=$root_token VAULT_TOKEN=$root_token
@@ -44,12 +49,18 @@ infrastructure() {
# Resolve through Docker DNS while verifying the certificate for localhost. # Resolve through Docker DNS while verifying the certificate for localhost.
https() { curl -sS --max-time 5 --cacert "$STATE/ca.pem" --connect-to localhost:5126:frontend:8443 "$@"; } https() { curl -sS --max-time 5 --cacert "$STATE/ca.pem" --connect-to localhost:5126:frontend:8443 "$@"; }
product() { product() {
compose ps --format json | jq -se 'length==6 and all(.[];.Health=="healthy")' >/dev/null || fail 'Expected six healthy services' expected=6
if [ -f "$STATE/sendico/settings.env" ]; then expected=7; fi
compose ps --format json | jq -se --argjson expected "$expected" 'length==$expected and all(.[];.Health=="healthy")' >/dev/null || fail "Expected $expected healthy services"
https -f https://localhost:5126/ >/dev/null https -f https://localhost:5126/ >/dev/null
https -f https://localhost:5126/api/v1/health >/dev/null https -f https://localhost:5126/api/v1/health >/dev/null
echo 'PASS: six healthy services, frontend and proxied BFF use verified TLS; no email sent' if [ "$expected" = 7 ]; then compose exec -T sendico sh /etc/sendico/check.sh; fi
echo "PASS: $expected healthy services, frontend and proxied BFF use verified TLS; no email sent"
} }
layout() { layout() {
if [ -f "$STATE/sendico/settings.env" ]; then
kv services/bff/sendico | jq -e 'keys==["client_secret","login","password"]' >/dev/null || fail 'Sendico secret contains non-secret configuration'
fi
kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault' kv services/notifications/email/resend | jq -e 'keys==["api_key"]' >/dev/null || fail 'Non-secret email configuration remains in Vault'
for old in bootstrap/mongo bff caller services/notifications/callers/bff email nats tls frontend-tls notifications-tls; do for old in bootstrap/mongo bff caller services/notifications/callers/bff email nats tls frontend-tls notifications-tls; do
[ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old" [ "$(vault "kv/metadata/$old" GET yes)" = null ] || fail "Obsolete KV entry remains: $old"
+11
View File
@@ -0,0 +1,11 @@
#!/bin/sh
set -eu
umask 077
check=$(mktemp -d /private/check.XXXXXX)
trap 'rm -rf "$check"' EXIT
printf 'Authorization: Bearer %s\n' "$(cat /run/sendico/access-token)" > "$check/header"
for path in "payments/fx/currencies/$SENDICO_ORGANIZATION_REF" "ledger_accounts/$SENDICO_ORGANIZATION_REF"; do
curl -fsS --max-time 15 -H "@$check/header" "http://127.0.0.1:18081/api/v1/$path" > "$check/response" 2>/dev/null || { echo 'Local Sendico authenticated API check failed' >&2; exit 1; }
jq -e '.status=="success"' "$check/response" >/dev/null
done
echo 'PASS: local Sendico authenticated currency and ledger APIs are accessible'
+30
View File
@@ -0,0 +1,30 @@
# Enabled by make sendico; settings contain no credentials.
services:
bff:
environment:
SXCHNGE_SENDICO_LOCAL: '1'
volumes:
- ./.local/dev/sendico/runtime.yml:/etc/sxchnge/bff.yml:ro
- sendico-session:/run/sendico:ro
sendico:
image: sxchnge-local/tools:dev
restart: unless-stopped
# Keep the adapter's loopback-only HTTP rule; forward to the public stand API.
network_mode: service:bff
entrypoint: [/bin/sh, /etc/sendico/session.sh]
env_file: ./.local/dev/sendico/settings.env
volumes:
- ./infra/local/sendico/session.sh:/etc/sendico/session.sh:ro
- ./infra/local/sendico/check.sh:/etc/sendico/check.sh:ro
- ./.local/dev/agents/sendico:/etc/agent:ro
- sendico-session:/run/sendico
tmpfs: [/private:mode=0700]
healthcheck:
test: [CMD-SHELL, 'test -f /private/ready && test $$(($$(date +%s) - $$(stat -c %Y /private/ready))) -lt 1500 && curl -fsS --max-time 2 http://127.0.0.1:18081/api/v1/health >/dev/null']
interval: 10s
timeout: 3s
retries: 30
volumes:
sendico-session:
+45
View File
@@ -0,0 +1,45 @@
#!/bin/sh
set -eu
umask 077
mkdir -p /private /run/sendico
chmod 755 /run/sendico
cd /private
socat TCP4-LISTEN:18081,bind=127.0.0.1,reuseaddr,fork TCP4:host.docker.internal:8080 &
bridge=$!
trap 'kill "$bridge" 2>/dev/null || true' EXIT
trap 'exit 0' INT TERM
session() {
jq -n --rawfile role /etc/agent/role-id --rawfile secret /etc/agent/secret-id \
'{role_id:($role|rtrimstr("\n")),secret_id:($secret|rtrimstr("\n"))}' > login.json
curl -fsS --max-time 15 -H 'Content-Type: application/json' --data-binary @login.json \
http://vault:8200/v1/auth/approle/login > vault.json 2>/dev/null || return 1
jq -er '"X-Vault-Token: "+.auth.client_token' vault.json > vault.header || return 1
curl -fsS --max-time 15 -H @vault.header http://vault:8200/v1/kv/data/services/bff/sendico > credentials.json 2>/dev/null || return 1
jq -e --arg client "$SENDICO_CLIENT_ID" --arg device "$SENDICO_DEVICE_ID" \
'.data.data | {login:{login,password},clientSecret:.client_secret,clientId:$client,deviceId:$device}' credentials.json > request.json || return 1
curl -fsS --max-time 15 -H @vault.header -X POST http://vault:8200/v1/auth/token/revoke-self >/dev/null 2>&1 || return 1
rm -f vault.header vault.json credentials.json login.json
curl -fsS --max-time 20 -H 'Content-Type: application/json' --data-binary @request.json \
http://127.0.0.1:18081/api/v1/accounts/login/api > response.json 2>/dev/null || return 1
rm -f request.json
jq -er '.data.accessToken.token' response.json > token 2>/dev/null || return 1
jq -e --arg source "$SENDICO_SOURCE_ACCOUNT_REF" \
'.data.accessToken.token | split(".")[1] | gsub("-";"+") | gsub("_";"/") | @base64d | fromjson | .id==$source and .pending!=true and .exp>(now+1500)' response.json >/dev/null 2>&1 || return 1
cp token /run/sendico/access-token.new
chmod 444 /run/sendico/access-token.new
mv /run/sendico/access-token.new /run/sendico/access-token
rm -f token response.json
touch ready
}
while :; do
if session; then
echo 'Local Sendico session ready; next renewal in 20 minutes.'
sleep 1200 & wait $!
else
echo 'Local Sendico session unavailable; check stand health and integration credentials. Retrying in 5 minutes.' >&2
rm -f request.json response.json token vault.header vault.json credentials.json login.json
sleep 300 & wait $!
fi
done