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
| Tool | Version | Notes |
|---|---|---|
| Node.js | 20 LTS | nvm |
| pnpm | 9.x | corepack enable |
| Docker Engine | 24+ | Compose v2 |
grpcurl | latest | gRPC testing |
psql | 16 | Postgres client |
redis-cli | 7 | Redis client |
nats CLI | 0.1.5+ | NATS testing |
openssl | 3.x | Test cert generation |
pkcs11-tool | latest | HSM session test |
jq | latest | — |
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:
| File | Purpose |
|---|---|
national-pki-root-ca.pem | Test root CA (used as trust anchor in HSM_TRUST_ANCHORS) |
national-pki-intermediate-ca.pem | Intermediate 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.txt | PIN for softhsm test token |
init-token.sh | Script to initialise softhsm + import caller certs |
crl.pem | Test 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
| Variable | Default (dev) | Purpose |
|---|---|---|
NODE_ENV | development | — |
LOG_LEVEL | debug | Pino level |
DATABASE_URL | postgres://... | cbc schema |
REDIS_URL | redis://... | Hot cache |
NATS_URL | nats://nats:4222 | Event bus |
GRPC_PORT | 50061 | — |
HTTP_PORT | 3061 | Admin + health |
GRPC_TLS_ENABLED | false | mTLS off in dev; true in prod |
HSM_PKCS11_LIB | softhsm path | PKCS#11 library |
HSM_TOKEN_LABEL | cbc-dev-token | softhsm token |
HSM_PIN_FILE | /test-certs/hsm-pin.txt | PIN source |
REGION | dev | Region label |
CBE_ADAPTER_* | Per-MNO | Adapter selection |
CBE_ENDPOINT_* | Per-MNO | Mock endpoint URLs |
DISPATCH_TIMEOUT_SECONDS | 30 | Per-MNO dispatch timeout |
BROADCAST_REPLAY_WINDOW_SECONDS | 300 | Signature timestamp window |
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
/health/ready returns 503 | HSM not initialised, or token missing | Run init-token.sh |
gRPC returns UNAUTHENTICATED for valid cert | Cert not in cbc.authorised_callers seed | Reseed DB |
gRPC returns REPLAY_NONCE | Client clock drift > 5 min | docker compose restart for NTP alignment |
| CBE mock returns success but no NATS event | NATS container healthy? outbox-relay log? | Check docker compose logs cbc-bridge for outbox errors |
| Cell resolution returns "unknown" | Seed missing for that cell | Add to cbc.mno_cell_database seed |
| Drill schedule not firing | Cron worker container not running | Check docker compose ps and logs for cbc-bridge-drill-scheduler |
HSM pod log CKR_TOKEN_NOT_PRESENT | softhsm token wiped | Re-run init-token.sh |
9. Optional: Real-PKI Dev Mode
If your team has a test issuer from a real Afghan-PKI sandbox:
- Replace
test-certs/national-pki-root-ca.pemwith the sandbox root CA. - Replace caller test certs with actual test-issued certs.
- Adjust
HSM_TRUST_ANCHORSenv var. docker compose upcontinues 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).