# SMS Bridge — Detailed Build & Operating Notes

Project: `/home/administrator/sms-bridge/` · Live: `https://sms.sevenkoapps.store`

## System map

| Component | Detail |
|---|---|
| Backend API + dashboard | Node stdlib (`node:sqlite`), **host systemd service** `sms-bridge.service` → `server/index.js` |
| Bind / port | `Environment=BIND=0.0.0.0`, TCP **4090** |
| Database | `data/smsbridge.db` (SQLite, WAL) |
| HTTP exposure | Behind Caddy (`admin-caddy` container, `admin-panel_default` net), route `sms.sevenkoapps.store → 172.19.0.1:4090`, TLS via Let's Encrypt |
| ufw | Port 4090 open only to docker bridges `172.19.0.0/16`, `172.18.0.0/16` — no public direct access |
| Android app | `android/`, pure framework (zero gradle deps), minSdk 24 / targetSdk 36, package `co.sevenkoapps.smsbridge` |
| APK | `downloads/latest.apk` served by the bridge at `/apk/latest.apk` |
| Release keystore | `keystore/smsbridge-release.keystore` (alias `smsbridge`), creds in `~/.secrets/smsbridge-keystore.env`, off-box copy in `/home/administrator/cs001-backup-aug9/smsbridge-release.keystore` |
| Secrets | `~/.secrets/sms-bridge.env` (`SMSBRIDGE_ADMIN_PASSWORD`) |
| Scripts dir | `data/scripts/*.sh` (keyword autorun, run as user `administrator`) |

## Data model

- **devices** — `id, name, device_key(unique), active, created_at, last_seen, heartbeat`
  - `device_key` is the app's persistent random ID (`sbd_…`); **auto-registered on first contact** (old `token_hash/token_prefix` migrated → `device_key` via table rebuild).
- **api_keys** — `name, key_hash(sha256), key_prefix, scopes("send","read",csv), active, created_at, last_used_at`. Key shown once; only hash stored.
- **messages** — `direction(in|out), address, body, status, device_id, api_key_id, request_id, hash(dedupe), created_at, updated_at`
  - outbound `status`: `queued → sent → delivered | failed`
  - inbound `status`: `received`
  - queued outbound expires → `failed` (no device pickup)
- **keywords** — `keyword, match_mode(exact|starts|contains), action_webhook, action_script, reply_text, active, last_triggered`
- **webhook_log** — per-attempt delivery log (kind: `webhook|script`, status, detail)
- **apk_releases** — `version_code, version_name, notes, created_at`
- **settings** — `admin_password_hash, global_webhook, webhook_secret`

## Flow

1. **Send:** client → `POST /api/send` (API key) → row `queued` → phone polls `/api/device/pending` → `SmsManager` sends → reports `sent|delivered|failed`.
2. **Receive:** phone `SmsReceiver` → `POST /api/device/incoming` → server dedupes (hash), stores `received`, fires global webhook + keyword rules, queues auto-replies.
3. **Keyword:** exact/starts/contains match → optional webhook, optional script, optional SMS auto-reply.
4. **Webhook:** signed `X-SMSBridge-Signature: sha256=HMAC-SHA256(secret, raw body)`; retried ×2 (10s, 60s); all attempts logged.

## Zero-config device auth (v1.1.0)

- App generates persistent random `device_key` in SharedPreferences, sends as `x-device-key`.
- Server `authDevice()` auto-registers unknown key on first contact (name `phone-<key8>`); repeat contacts reuse the row (no dup).
- No token to provision/paste. Trade-off: any caller with the URL can register a device — acceptable because service is only reachable via Caddy HTTPS and ufw-restricted to docker bridges. Optional future hardening: one-time pairing code.

## Operations

```bash
systemctl status sms-bridge
sudo journalctl -u sms-bridge -f
sudo systemctl restart sms-bridge       # also drops in-memory admin sessions
curl -s https://sms.sevenkoapps.store/api/health
```

## Rebuild Android APK

```bash
cd /home/administrator/sms-bridge/android
sed -i 's/versionCode N/versionCode N+1/; s/versionName "x.y.z"/versionName "x.y.(z+1)"/' app/build.gradle
export ANDROID_HOME=/opt/android-sdk
./gradlew assembleRelease --no-daemon     # signing creds auto-loaded from gradle.properties
/opt/android-sdk/build-tools/36.0.0/apksigner verify --print-certs \
  app/build/outputs/apk/release/app-release.apk | head -1
/home/administrator/sms-bridge/scripts/publish_apk.sh "changelog note"
```

Signing creds live in `gradle.properties` (chmod 600) so release builds are deterministic
without `-P` flags. Keystore password also in `~/.secrets/smsbridge-keystore.env`.

## Version history

- **v1.0.0** (code 1) — initial bridge.
- **v1.1.0** (code 2) — **zero-config**; removed device token; devices auto-register via `x-device-key`; Devices table migrated.
- **v1.1.1** (code 3) — fix Start/Stop buttons greyed out on fresh install (Start no longer gated on saved config; circular lockout removed).

## Security posture

- Dashboard password + HttpOnly session cookie; login throttled (5 fails → 15 min lockout).
- API keys hashed (SHA-256), shown once.
- Webhook signatures (HMAC) — verify before trusting callbacks.
- Service binds 0.0.0.0 but ufw blocks non-docker public access; internet only via Caddy TLS.
- Zero runtime npm/external deps (stdlib only). Keystore + secrets never in repo/markdown (in `~/.secrets/`, mode 0600).