Skip to main content

cbc-bridge-service — Local Dev Setup

Version: 1.0 Status: Draft Owner: Government / Emergency + DevEx Last Updated: 2026-04-21 References: SERVICE_OVERVIEW.md, DEPLOYMENT_TOPOLOGY.md, TESTING_STRATEGY.md

Local development recipe for cbc-bridge-service. Because the service depends on HSM (PKI signature verification) and multiple MNO CBE adapters, the dev stack uses softhsm2 and three per-adapter mock CBE endpoints.


1. Prerequisites

ToolVersionNotes
Node.js20 LTSnvm
pnpm9.xcorepack enable
Docker Engine24+Compose v2
grpcurllatestgRPC testing
psql16Postgres client
redis-cli7Redis client
nats CLI0.1.5+NATS testing
openssl3.xTest cert generation
pkcs11-toollatestHSM session test
jqlatest

2. Repository Layout

Ghasi-SMS-Gateway/
services/cbc-bridge-service/ ← this service spec
(application repo)/services/cbc-bridge-service/ ← code
infra/docker/docker-compose.cbc-bridge.yml
infra/docker/cbc-bridge/
test-certs/ ← test national-PKI certs for dev
mock-cbe/ ← mock MNO CBE endpoint implementations
seed/ ← DB seed + cell-DB fixtures

3. Docker Compose Recipe

infra/docker/docker-compose.cbc-bridge.yml:

version: '3.9'
services:
postgres-cbc:
image: postgres:16-alpine
environment:
POSTGRES_DB: cbc
POSTGRES_USER: cbc
POSTGRES_PASSWORD: cbc
ports: ["5446:5432"]
volumes:
- cbc_pg_data:/var/lib/postgresql/data
- ./cbc-bridge/seed/schema.sql:/docker-entrypoint-initdb.d/001-schema.sql:ro
- ./cbc-bridge/seed/seed.sql:/docker-entrypoint-initdb.d/002-seed.sql:ro

redis-cbc:
image: redis:7-alpine
ports: ["6396:6379"]
command: redis-server --appendonly yes

nats:
image: nats:2.10-alpine
command: -js -m 8222
ports: ["4224:4222", "8224:8222"]

softhsm:
image: ghasi/softhsm-dev:2.6
ports: ["2345:2345"]
volumes:
- cbc_hsm_data:/var/lib/softhsm
- ./cbc-bridge/test-certs:/test-certs:ro

mock-cbe-awcc: # standard-3gpp adapter target
image: ghasi/mock-cbe-standard3gpp:dev
environment: { MNO_ID: "awcc", CBE_PORT: 9400 }
ports: ["9400:9400"]

mock-cbe-roshan: # Ericsson proprietary
image: ghasi/mock-cbe-ericsson:dev
environment: { MNO_ID: "roshan", CBE_PORT: 9401 }
ports: ["9401:9401"]

mock-cbe-etisalat: # standard-3gpp
image: ghasi/mock-cbe-standard3gpp:dev
environment: { MNO_ID: "etisalat-af", CBE_PORT: 9402 }
ports: ["9402:9402"]

mock-cbe-mtn: # Huawei proprietary
image: ghasi/mock-cbe-huawei:dev
environment: { MNO_ID: "mtn-af", CBE_PORT: 9403 }
ports: ["9403:9403"]

mock-cbe-salaam: # standard-3gpp
image: ghasi/mock-cbe-standard3gpp:dev
environment: { MNO_ID: "salaam", CBE_PORT: 9404 }
ports: ["9404:9404"]

cbc-bridge:
image: ghasi/cbc-bridge-service:dev
depends_on:
- postgres-cbc
- redis-cbc
- nats
- softhsm
- mock-cbe-awcc
- mock-cbe-roshan
- mock-cbe-etisalat
- mock-cbe-mtn
- mock-cbe-salaam
ports: ["3061:3061", "50061:50061"]
environment:
NODE_ENV: development
LOG_LEVEL: debug
DATABASE_URL: postgres://cbc:cbc@postgres-cbc:5432/cbc
REDIS_URL: redis://redis-cbc:6379/0
NATS_URL: nats://nats:4222
GRPC_PORT: 50061
HTTP_PORT: 3061
GRPC_TLS_ENABLED: "false" # dev-only — prod is mTLS
HSM_PKCS11_LIB: /softhsm/libsofthsm2.so
HSM_TOKEN_LABEL: cbc-dev-token
HSM_PIN_FILE: /test-certs/hsm-pin.txt
REGION: "dev"
# Per-MNO adapter configuration
CBE_ADAPTER_AWCC: "standard3gpp"
CBE_ENDPOINT_AWCC: "http://mock-cbe-awcc:9400"
CBE_ADAPTER_ROSHAN: "ericsson"
CBE_ENDPOINT_ROSHAN: "http://mock-cbe-roshan:9401"
CBE_ADAPTER_ETISALAT_AF: "standard3gpp"
CBE_ENDPOINT_ETISALAT_AF: "http://mock-cbe-etisalat:9402"
CBE_ADAPTER_MTN_AF: "huawei"
CBE_ENDPOINT_MTN_AF: "http://mock-cbe-mtn:9403"
CBE_ADAPTER_SALAAM: "standard3gpp"
CBE_ENDPOINT_SALAAM: "http://mock-cbe-salaam:9404"
DISPATCH_TIMEOUT_SECONDS: "30"
BROADCAST_REPLAY_WINDOW_SECONDS: "300"
volumes:
- ./cbc-bridge/test-certs:/test-certs:ro

volumes:
cbc_pg_data:
cbc_hsm_data:

Start:

docker compose -f infra/docker/docker-compose.cbc-bridge.yml up -d

Wait for softhsm to initialise, then seed test keys:

# Initialise softhsm token + import test caller certs
docker compose exec softhsm /test-certs/init-token.sh

Verify:

curl -s http://localhost:3061/health/ready | jq
grpcurl -plaintext localhost:50061 list

4. Test Cert Material

infra/docker/cbc-bridge/test-certs/ contains pre-generated (or generated on first docker compose up) test PKI material:

FilePurpose
national-pki-root-ca.pemTest root CA (used as trust anchor in HSM_TRUST_ANCHORS)
national-pki-intermediate-ca.pemIntermediate CA
caller-ndma.{crt,key}Test NDMA (Civil Defence) caller cert — authorised P0 + P1 + P2, all regions
caller-police.{crt,key}Test Police caller — authorised P1 only, Kabul region
caller-revoked.{crt,key}Test revoked caller — appears in test CRL
hsm-pin.txtPIN for softhsm test token
init-token.shScript to initialise softhsm + import caller certs
crl.pemTest CRL (contains revoked test cert)

Caller certs also loaded into cbc.authorised_callers in the seed SQL.

Regeneration (if needed):

cd infra/docker/cbc-bridge/test-certs
./regenerate-test-certs.sh

5. Seed Data

cbc-bridge/seed/seed.sql:

-- Authorised callers (test)
INSERT INTO cbc.authorised_callers (id, org_name, cert_subject_cn, allowed_severities, allowed_regions, not_before, not_after, mou_ref) VALUES
('caller_ndma_test', 'NDMA Civil Defence', 'CN=ndma-test.civildefence.af', '{P0,P1,P2}', '{*}', now() - interval '1 day', now() + interval '30 days', 'MoU-NDMA-2026'),
('caller_police_test', 'AF National Police', 'CN=police-test.af.gov', '{P1}', '{KBL}', now() - interval '1 day', now() + interval '30 days', 'MoU-POL-2026'),
('caller_revoked_test', 'Revoked Test', 'CN=revoked-test.local', '{P1}', '{KBL}', now() - interval '1 day', now() + interval '30 days', 'MoU-REV-2026');

-- Sample cell-tower coordinates per MNO (subset)
INSERT INTO cbc.mno_cell_database (mno, cell_id, lat, lng, accuracy_m, last_updated) VALUES
('awcc', 'awcc-cell-001', 34.5553, 69.2075, 500, now()),
('awcc', 'awcc-cell-002', 34.5333, 69.1833, 500, now()),
('roshan', 'roshan-cell-001', 34.5553, 69.2075, 500, now()),
('etisalat-af', 'et-cell-001', 36.7080, 67.1110, 500, now()), -- Mazar
('mtn-af', 'mtn-cell-001', 31.5780, 65.7460, 500, now()), -- Kandahar
('salaam', 'sal-cell-001', 34.5553, 69.2075, 500, now());

-- Drill schedule (monthly first Tuesday 10:00 Asia/Kabul)
INSERT INTO cbc.drill_schedule (id, cron_expression, active) VALUES
('drill_monthly', '0 10 1-7 * 2', true);

6. Common Commands

6.1 Verify gRPC shape

grpcurl -plaintext localhost:50061 list ghasi.cbc.v1.CbcBridgeService
grpcurl -plaintext localhost:50061 describe ghasi.cbc.v1.CbcBridgeService.BroadcastEmergency

6.2 Submit a test broadcast (signed with test NDMA cert)

A helper script scripts/sign-and-submit.js wraps PKI signing + gRPC call:

node scripts/sign-and-submit.js \
--cert test-certs/caller-ndma.crt \
--key test-certs/caller-ndma.key \
--severity P1 \
--geo '{"kind":"region","region":"KBL"}' \
--body '{
"en":"TEST: Flood warning in Kabul",
"fa":"آزمایش: هشدار سیلاب در کابل",
"ps":"ازمایښت: د کابل د سیلاب خبرداری",
"ar":"اختبار: تحذير من الفيضانات في كابل"
}'

Expected:

{
"broadcastId": "bc_...",
"acceptedAt": "2026-04-21T08:00:00Z",
"expectedDispatchBy": "2026-04-21T08:00:30Z"
}

6.3 Watch dispatch fan-out

nats sub 'cbc.broadcast.*'

6.4 Test a revoked cert

node scripts/sign-and-submit.js \
--cert test-certs/caller-revoked.crt \
--key test-certs/caller-revoked.key \
--severity P1 --geo '{"kind":"country","country":"AF"}' \
--body '{"en":"should fail","fa":"","ps":"","ar":""}'
# Expect UNAUTHENTICATED with reason CRL_REVOKED

6.5 Test an over-scope request

# Police cert can only do P1 in KBL region — attempt P0:
node scripts/sign-and-submit.js \
--cert test-certs/caller-police.crt \
--key test-certs/caller-police.key \
--severity P0 --geo '{"kind":"country","country":"AF"}' \
--body '{"en":"unauthorised","fa":"","ps":"","ar":""}'
# Expect UNAUTHORIZED_SEVERITY or UNAUTHORIZED_REGION

6.6 Cancel a broadcast (dual-control)

# Initiate
node scripts/cancel.js --broadcast-id bc_abc --role initiator --cert caller-ndma.crt --key caller-ndma.key
# Approve (separate terminal, within 60 s)
node scripts/cancel.js --broadcast-id bc_abc --role approver --cert caller-ndma-approver.crt --key caller-ndma-approver.key

6.7 Trigger a drill now

curl -X POST http://localhost:3061/v1/admin/cbc/drill/now \
-H "Authorization: Bearer $ADMIN_JWT" \
-d '{"geoTarget":{"kind":"country","country":"AF"}}'

6.8 Inspect audit chain

psql postgres://cbc:cbc@localhost:5446/cbc -c "
SELECT id, ts, event_type, encode(record_hash, 'hex') as record_hash
FROM cbc.audit
ORDER BY ts DESC LIMIT 10;
"

6.9 Verify chain integrity

pnpm --filter @ghasi/cbc-bridge-service audit:verify -- --from 2026-04-01 --to 2026-04-21

6.10 Force-break the chain (testing the verifier)

psql ... -c "UPDATE cbc.audit SET payload = payload || '{\"injected\":true}' WHERE id = 'aud_known'"
# Then re-run verifier — expect detection
pnpm --filter @ghasi/cbc-bridge-service audit:verify -- --from 2026-04-01 --to 2026-04-21

6.11 Refresh cell database from mock

curl -X POST http://localhost:3061/v1/admin/cbc/cell-db/refresh \
-H "Authorization: Bearer $ADMIN_JWT"

6.12 Run tests

pnpm --filter @ghasi/cbc-bridge-service test
pnpm --filter @ghasi/cbc-bridge-service test:integration
pnpm --filter @ghasi/cbc-bridge-service test:contract
pnpm --filter @ghasi/cbc-bridge-service test:e2e:local

6.13 Reset state

docker compose -f infra/docker/docker-compose.cbc-bridge.yml down -v
docker compose -f infra/docker/docker-compose.cbc-bridge.yml up -d
docker compose -f infra/docker/docker-compose.cbc-bridge.yml exec softhsm /test-certs/init-token.sh

7. Environment Variables Reference

VariableDefault (dev)Purpose
NODE_ENVdevelopment
LOG_LEVELdebugPino level
DATABASE_URLpostgres://...cbc schema
REDIS_URLredis://...Hot cache
NATS_URLnats://nats:4222Event bus
GRPC_PORT50061
HTTP_PORT3061Admin + health
GRPC_TLS_ENABLEDfalsemTLS off in dev; true in prod
HSM_PKCS11_LIBsofthsm pathPKCS#11 library
HSM_TOKEN_LABELcbc-dev-tokensofthsm token
HSM_PIN_FILE/test-certs/hsm-pin.txtPIN source
REGIONdevRegion label
CBE_ADAPTER_*Per-MNOAdapter selection
CBE_ENDPOINT_*Per-MNOMock endpoint URLs
DISPATCH_TIMEOUT_SECONDS30Per-MNO dispatch timeout
BROADCAST_REPLAY_WINDOW_SECONDS300Signature timestamp window

8. Troubleshooting

SymptomLikely causeFix
/health/ready returns 503HSM not initialised, or token missingRun init-token.sh
gRPC returns UNAUTHENTICATED for valid certCert not in cbc.authorised_callers seedReseed DB
gRPC returns REPLAY_NONCEClient clock drift > 5 mindocker compose restart for NTP alignment
CBE mock returns success but no NATS eventNATS container healthy? outbox-relay log?Check docker compose logs cbc-bridge for outbox errors
Cell resolution returns "unknown"Seed missing for that cellAdd to cbc.mno_cell_database seed
Drill schedule not firingCron worker container not runningCheck docker compose ps and logs for cbc-bridge-drill-scheduler
HSM pod log CKR_TOKEN_NOT_PRESENTsofthsm token wipedRe-run init-token.sh

9. Optional: Real-PKI Dev Mode

If your team has a test issuer from a real Afghan-PKI sandbox:

  1. Replace test-certs/national-pki-root-ca.pem with the sandbox root CA.
  2. Replace caller test certs with actual test-issued certs.
  3. Adjust HSM_TRUST_ANCHORS env var.
  4. docker compose up continues to work — softhsm remains the HSM mock.

For full HSM-real dev, request an HSM test appliance from Security team (not typical for local dev).