File size: 11,564 Bytes
70650b7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
# M22 β€” Mobile Native Client

**Spec version:** v1.0 (Phase 2)
**Depends on:** M01 (identity), M15 (relay tier for push and NAT traversal), M16 (tokens for app auth), M23 (E2E for chat), X06 (WebSocket for live updates), the entire Phase 1 bus protocol as a wire client
**Depended on by:** end users on iOS/Android β€” first non-web HearthNet surface

---

## 1. Responsibility

Native mobile application that:

- Onboards into a community (scan invite QR or paste invite blob)
- Stores keys in the device's secure enclave (iOS Keychain, Android Keystore)
- Receives push notifications via the relay tier (M15)
- Calls the community's anchor(s) via the bus protocol over HTTP/SSE or WebSocket
- Provides UI for chat (1:1 + group), marketplace, ask (LLM), community feed
- Operates fully when the user's anchor is reachable; degrades gracefully when not

This module specifies **the contract between the mobile client and the rest of HearthNet**. The actual Flutter codebase lives in a separate repo (`/mobile-native`) and is not Python.

---

## 2. File layout

```
mobile-native/                          # separate Flutter project
β”œβ”€β”€ pubspec.yaml
β”œβ”€β”€ README.md
β”œβ”€β”€ lib/
β”‚   β”œβ”€β”€ main.dart
β”‚   β”œβ”€β”€ onboarding/                     # invite scan, key gen
β”‚   β”œβ”€β”€ identity/                       # key storage, signing
β”‚   β”‚   β”œβ”€β”€ secure_storage.dart
β”‚   β”‚   └── signing.dart
β”‚   β”œβ”€β”€ bus/                            # protocol client
β”‚   β”‚   β”œβ”€β”€ http_client.dart
β”‚   β”‚   β”œβ”€β”€ ws_client.dart
β”‚   β”‚   └── sse_client.dart
β”‚   β”œβ”€β”€ crypto/                         # E2E using cryptography_flutter / libsodium bindings
β”‚   β”‚   β”œβ”€β”€ x25519.dart
β”‚   β”‚   β”œβ”€β”€ ratchet.dart
β”‚   β”‚   └── envelope.dart
β”‚   β”œβ”€β”€ push/                           # APNs / FCM hookup
β”‚   β”‚   └── subscriber.dart
β”‚   β”œβ”€β”€ ui/                             # screens
β”‚   β”‚   β”œβ”€β”€ chat.dart
β”‚   β”‚   β”œβ”€β”€ marketplace.dart
β”‚   β”‚   β”œβ”€β”€ ask.dart
β”‚   β”‚   └── community.dart
β”‚   └── settings/
β”œβ”€β”€ ios/
β”œβ”€β”€ android/
└── tests/

hearthnet/mobile/                        # Python-side helper (in the main package)
β”œβ”€β”€ __init__.py
β”œβ”€β”€ invite.py                            # mobile-targeted invite QR generation
└── push_authority.py                    # bus-side service for mobile push token registry
```

The Python `hearthnet/mobile/` package contains the anchor-side helpers used by the existing community. The Flutter code is its own world; this spec governs the wire contract it must implement.

---

## 3. Onboarding flow

```
User installs HearthNet app
  ↓
App: "Scan invite QR or paste invite link"
  ↓
On scan: parse hnvite:// blob (Phase 1 Β§13)
  ↓
App generates Ed25519 keypair via libsodium binding
  Persists private key in iOS Keychain (kSecAttrAccessibleAfterFirstUnlock)
  or Android Keystore (KeyProperties.AUTH_REQUIRED if biometric set)
  ↓
App calls bus.call("onboard.complete", input={
    "invite_token": "...",
    "node_id_full": "<our new ed25519 pub>",
    "device_class": "mobile",
    "display_name": "<user-entered>",
    "platform": "ios|android",
})
  ↓
Anchor processes invite (M13), emits node.joined event
  ↓
App fetches community manifest (signed); pins it
  ↓
App registers for push:
  - obtain APNs/FCM device token from OS
  - bus.call("relay.push.register", input={device_token, platform})
  ↓
App publishes E2E prekey bundle (e2e.prekeys.published event)
  ↓
Ready to use
```

---

## 4. Bus protocol on mobile

### 4.1 Same wire as desktop

The mobile client speaks the same HTTP/SSE/WebSocket protocol as Phase 1 anchors. It is a **client** that calls into anchors/services in its community; it does not host capabilities itself (mostly β€” see Β§4.4).

### 4.2 Endpoint selection

The mobile client maintains an ordered list of endpoints:

1. **Cached anchor endpoints** from last successful manifest fetch
2. **Local network discovery** (mDNS via [Bonjour iOS / NSD Android]) β€” only when on Wi-Fi
3. **Relay tier** (M15) β€” for NAT traversal when on cellular
4. **DHT lookup** (X05) β€” last resort

Per call, the client tries endpoints in order; first success wins. Persistent failures bubble up as `relay_unreachable` or `network_unreachable`.

### 4.3 Reconnection

WebSocket connections are persistent for tool-call loops and live pubsub. On disconnect, the client reconnects with exponential backoff (1s, 2s, 4s, ..., capped at 60s). Reconnect re-subscribes to all active topics.

### 4.4 Mobile-as-callable

In Phase 2, the mobile client does NOT register capabilities back into the community. It's purely a caller. Phase 3 may allow simple things (`market.list` from local cache while offline).

### 4.5 Token-bearer mode

For background-fetch (when the app is suspended), the OS may run a brief task. Background tasks use a **bearer token** from M16 (issued at onboarding, refreshed on each app foreground). The token has scope:

```json
{
  "capabilities": ["chat.fetch","marketplace.list"],
  "rate_limit_per_minute": 30
}
```

This avoids needing the user's biometric to unlock the private key for background polling.

---

## 5. Push notifications

### 5.1 What triggers a push

When the following events occur in the community, anchors send a push to relevant subscribed mobile devices:

- `chat.message.sent` where recipient is the mobile user
- `chat.thread.message.sent` where mobile user is a thread member
- `marketplace.post.created` where post matches user's subscribed categories
- `community.alert` (broadcast emergency alert)
- `node.joined` (subscribed users only)

### 5.2 Push payload shape

Per [M15 Β§5.4](M15-relay-tier.md), the payload is minimal:

```json
{
  "event_type": "chat.message.sent",
  "sender_short": "7H4G-...",
  "preview": "Hallo Jana, ich bring..."     // optional cleartext preview if not E2E
}
```

For E2E messages, the preview is absent and the app must fetch + decrypt on open.

### 5.3 iOS vs Android specifics

**iOS:** APNs payload with `aps.alert.title` and `aps.alert.body`. Background mode enabled to fetch on receive (`content-available: 1`).

**Android:** FCM `data` message (not `notification` β€” we control display). Handled by the app's `FirebaseMessagingService`.

### 5.4 Quiet hours

User-configurable. Push silenced 22:00–07:00 by default; emergency alerts override.

### 5.5 Mute and per-thread settings

Per-thread mute, per-category marketplace silence. Stored in `mobile.preferences` event (self-only, encrypted-at-rest on device).

---

## 6. Secure key storage

### 6.1 iOS β€” Keychain Services

```dart
// lib/identity/secure_storage.dart (sketch)
const _accessibility = 'kSecAttrAccessibleAfterFirstUnlock';

Future<void> storePrivateKey(String label, Uint8List bytes) async {
  await KeychainAccess.setData(
    label: label,
    data: bytes,
    accessibility: _accessibility,
    accessControl: AccessControl.userPresence,  // biometric on key use
  );
}
```

### 6.2 Android β€” Keystore

Hardware-backed if available (StrongBox on modern Pixels), else TEE.

```dart
final cipher = await CryptographyFlutter.aesGcm(
  keyId: 'hearthnet_identity_v1',
  requireAuth: AuthMethod.biometric,
);
```

### 6.3 Backup

Private keys are NEVER backed up via iCloud / Google Backup. App-level backup uses an encrypted export blob (user-chosen passphrase) that the user is expected to save out-of-band (e.g. password manager, written down).

`config.mobile.cloud_backup_allowed = false` enforced.

### 6.4 Lost device

If the user loses the device, they:

1. Wait out the device's session β€” eventually messages stop delivering
2. From another device, call `node.revoke` on this NodeID (anchor co-signs)
3. The revoked NodeID is then blacklisted; the lost phone, even if recovered, can't authenticate

This is identical to the Phase 1 node revocation flow.

---

## 7. UI surface (mobile)

Mirrors the web UI ([M08](../../modules/M08-ui.md)) but native:

| Tab | Content |
|-----|---------|
| **Chat** | 1:1 conversations + group threads. Live updates via WS. Voice notes optional (record β†’ STT β†’ send transcript + audio attachment). |
| **Market** | List + post; image attachments via camera/gallery. |
| **Ask** | LLM chat. Tool-augmented mode available. Voice input button. |
| **Community** | Member list, recent events, federation peers. |
| **Settings** | Push prefs, language, backup, advanced. |

All UI is plain Material/Cupertino β€” no exotic frameworks. Matches Christof's preference for boring, durable tech.

---

## 8. Configuration

Mobile-side (lives in app):

```dart
const config = {
  'community_id': '<from invite>',
  'anchor_endpoints': [/* from manifest */],
  'relay_url': 'https://relay.hearthnet.de',
  'push_enabled': true,
  'quiet_hours': {'start': '22:00', 'end': '07:00'},
  'background_fetch_minutes': 15,
};
```

Anchor-side (Python, for the push_authority service):

```python
config.mobile.push_enabled              = True
config.mobile.push_categories_marketplace = ["essentials","emergency"]
config.mobile.push_quiet_hours_default  = ("22:00","07:00")
```

---

## 9. Errors

| Condition | UI presentation |
|-----------|-----------------|
| No network | Offline banner; queued sends |
| Anchor unreachable | "Your community anchor is offline. Retrying..." |
| Relay unreachable | Falls back to direct; warns if all fail |
| Token expired | Silent refresh; only surface if refresh fails |
| Push delivery failed | No UI; logged for diagnostics |
| Manifest signature mismatch | Hard block; re-onboard required |

---

## 10. Tests

### Flutter side
- Widget tests for each tab
- Integration test for onboarding flow with a mock anchor
- E2E test using a real anchor in CI (Linux runner running hearthnet)

### Anchor-side Python
- `test_push_subscription_recorded`
- `test_push_dispatch_on_chat_message_sent`
- `test_quiet_hours_silences_non_emergency`
- `test_revocation_revokes_mobile_session`

### Manual
- iOS + Android smoke tests on physical devices
- Background-fetch verified across 30-minute suspensions

---

## 11. Cross-references

| What | Where |
|------|-------|
| Bus protocol | [Phase 1 CAP Β§5](../../CAPABILITY_CONTRACT.md) |
| Push relay tier | [M15 Β§5.4](M15-relay-tier.md) |
| Token-bearer auth | [M16 Β§5.5](M16-tokens.md) |
| E2E chat | [M23](M23-e2e-encryption.md) |
| WebSocket | [X06](../cross-cutting/X06-websocket.md) |
| Invite blobs | [Phase 1 CAP Β§13](../../CAPABILITY_CONTRACT.md) |
| Web UI (mirror) | [M08](../../modules/M08-ui.md) |

---

## 12. Open questions

1. **Flutter vs React Native vs native (Swift + Kotlin).** Choosing Flutter for shared codebase and Christof's stated preference for boring/durable stacks. Reconsider if Flutter's keychain support is shaky.
2. **End-to-end encryption library in Flutter.** Need a libsodium binding that matches the Python side bit-exactly. `flutter_sodium` is well-maintained; verify on both platforms.
3. **Background fetch reliability.** iOS throttles aggressively. We accept "best effort"; push is the real delivery mechanism.
4. **Offline mode depth.** Mobile-only LLM (small Phi-3 / Gemma 2B) is Phase 3.
5. **Web push for PWA.** Could the same flow target a PWA (no native app)? Yes, with FCM web push; documented but not built in Phase 2.
6. **Family-share licence.** Christof might want to ship the iOS app to family members under his account; App Store policy permits this within Family Sharing.