@mhome/app-facade-protocol 1.40.1 → 1.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,10 +6,11 @@ first implementation, not a requirement. Commissioners are the native Clients:
6
6
  desktop/CLI Clientd, Android and iOS. The shared UI and CLI never talk BLE; they
7
7
  call the `/local/pod/*` targets of their native Client.
8
8
 
9
- Commissioning order is fixed: secure BLE session, Wi-Fi joined, user
10
- authorization, credential issue, delivery, activation. A credential is issued only
11
- after the pod reports a joined network. Failed setup releases the connection;
12
- pending cloud credentials expire, and a fresh successful claim cleans up old credentials.
9
+ Network provisioning and activation are separate transactions. BLE provisions
10
+ Wi-Fi; Ethernet and already-connected Wi-Fi skip it. Every initialization then
11
+ uses a direct native Client ↔ Pod LAN security-2 session. No Core relay is used.
12
+ The current credential API below is the existing personal Pod credential; this
13
+ transport change does not implement the separately planned Space Client issuer.
13
14
 
14
15
  ## 1. Device identity
15
16
 
@@ -47,7 +48,7 @@ credential invalidation, or inside a re-provision window. A commissioned pod doe
47
48
 
48
49
  ## 3. Secure session
49
50
 
50
- Transport is ESP protocomm over BLE GATT. Endpoints are GATT characteristics
51
+ Wi-Fi provisioning uses ESP protocomm over BLE GATT. Endpoints are GATT characteristics
51
52
  named by their Characteristic User Description descriptor (`0x2901`). A
52
53
  commissioner that cannot read descriptors falls back to fixed UUIDs: the service
53
54
  UUID with bytes 2–3 replaced by `prov-ctrl` `0xFF4F`, `prov-scan` `0xFF50`,
@@ -58,7 +59,7 @@ custom endpoints in table order from `0xFF54` (`pod-info` `0xFF54`,
58
59
  - Security: protocomm security 2 (SRP6a 3072-bit with SHA-512, then AES-256-GCM).
59
60
  Username `meow`. Password: a 6-digit decimal code.
60
61
  - Pods with a display generate a uniformly random code, precompute salt and
61
- verifier at boot, and show the code only while a central is connected. A new
62
+ verifier at boot, and show it while connected over BLE or available for LAN initialization. A new
62
63
  code is generated after a successful commissioning, after 5 failed handshakes,
63
64
  or 10 minutes after it was first shown. Pods without a display are out of scope
64
65
  for v1.
@@ -70,8 +71,8 @@ custom endpoints in table order from `0xFF54` (`pod-info` `0xFF54`,
70
71
  `M` uses it as sent; `u` pads both to 384 bytes.
71
72
  - The record nonce is 8 random bytes followed by a 32-bit big-endian counter
72
73
  starting at 1, advanced after every encrypt and decrypt on both sides
73
- (security 2 patch version 1; patch 0 keeps it fixed). Commissioners accept
74
- patch versions 0 and 1 only. A lost or failed exchange leaves the two counters
74
+ (security 2 patch version 1; patch 0 keeps it fixed). Commissioners require
75
+ patch version 1; fixed-nonce patch 0 is rejected. A lost or failed exchange leaves the two counters
75
76
  unknown, so the commissioner abandons the secure session after any transport or
76
77
  decryption failure. A new `SessionCmd0` restarts the handshake on the same link.
77
78
  - While a device refuses handshakes after repeated wrong codes it clears the
@@ -81,8 +82,8 @@ custom endpoints in table order from `0xFF54` (`pod-info` `0xFF54`,
81
82
  such as Insufficient Authorization), so after every rejected handshake the
82
83
  commissioner reads `proto-ver` again and fails the session with `code_locked`
83
84
  when either field is present. A pod session also fails with `code_locked` after
84
- the fifth rejected code. ESP pods restart with a new code after 5 failed
85
- handshakes instead of reporting a lockout.
85
+ the fifth rejected code. Pods generate a fresh code after five rejected handshakes. Once network
86
+ configuration is saved, code rotation preserves that network.
86
87
  - One BLE connection at a time; a second central receives a disconnect.
87
88
  - Session deadline: 10 minutes from connect, or 2 minutes waiting for a
88
89
  handshake. On expiry the pod discards all unpersisted state.
@@ -95,15 +96,17 @@ custom endpoints in table order from `0xFF54` (`pod-info` `0xFF54`,
95
96
  | `prov-config` | yes | standard Wi-Fi set/apply/status |
96
97
  | `prov-ctrl` | yes | standard reset after a failed join, before a retry |
97
98
  | `pod-info` | yes | device information |
98
- | `pod-credential` | yes | credential delivery and activation status |
99
+ | `pod-credential` | yes | LAN credential delivery/status/finish; BLE accepts only reprovision status/finish |
99
100
 
100
- Custom endpoint bodies are UTF-8 JSON, at most 480 bytes in each direction.
101
+ Custom endpoint bodies are UTF-8 JSON, at most 480 bytes over BLE. LAN requests
102
+ and responses are bounded to 64 KiB including encryption overhead.
101
103
 
102
104
  `pod-info` request `{}`, response:
103
105
 
104
106
  ```json
105
107
  {
106
108
  "protocol": 1,
109
+ "env": "prod",
107
110
  "deviceId": "a0b1c2d3e4f5",
108
111
  "model": "S1",
109
112
  "platform": "esp32c5",
@@ -114,6 +117,9 @@ Custom endpoint bodies are UTF-8 JSON, at most 480 bytes in each direction.
114
117
  }
115
118
  ```
116
119
 
120
+ `env` must match the native Client runtime (`prod`, `dev`, `e2e`).
121
+ `podId` is included when activation is in flight or durably completed, so a
122
+ new session can observe completion without issuing another credential.
117
123
  `platform` is diagnostic only. `wifiConfigured` is true when the pod kept a
118
124
  working network across logout; the commissioner then skips the Wi-Fi steps after
119
125
  confirming connectivity with `prov-config` status. Inside a Wi-Fi change window
@@ -146,21 +152,52 @@ Activation error codes: `time_sync_failed`, `cloud_unreachable`,
146
152
  `refresh_rejected`, `auth_rejected`, `storage_failed` (the final commit, or in a
147
153
  Wi-Fi change window saving the new network, failed).
148
154
 
149
- The pod persists Wi-Fi, credential, endpoints and Space in one commit after a
150
- successful activation. Nothing from an abandoned attempt survives it; an
151
- implementation whose Wi-Fi driver caches the network on its own (ESP-IDF
152
- provisioning does) erases that cache when the attempt ends.
155
+ The Pod commits Wi-Fi immediately after a successful join, before any credential
156
+ issuance. Activation commits only identity/binding data. Failed activation,
157
+ disconnection, code expiry and reboot preserve the successfully saved network.
158
+ A failed join rolls back only the incomplete network change.
159
+
160
+ ### LAN discovery and channel
161
+
162
+ - Service: `_meow-pod-init._tcp.local.` while network-ready and uninitialized.
163
+ TXT: full `deviceId`, `v=1`, `env`, optional `model`; SRV/A/AAAA locate the
164
+ endpoint. A failed or expired pairing attempt does not remove this service.
165
+ - The same service works on Ethernet and Wi-Fi. BLE supplies the full device ID
166
+ and identity over its secure session; the short display ID is never the match
167
+ key. Discovery data is untrusted and is checked against encrypted `pod-info`.
168
+ - HTTP POST `/{endpoint}` with `Content-Type: application/octet-stream` and a
169
+ fresh UUID `X-Meow-Session`. Allowed endpoints: `proto-ver`, `prov-session`,
170
+ `pod-info`, `pod-credential`. Wi-Fi endpoints are unavailable over LAN.
171
+ Clients connect to the resolved socket directly, with proxies/redirects off.
172
+ - Run a fresh Security 2 patch-1 handshake on LAN. After BLE, reuse only the
173
+ temporarily held code, never the BLE session key or nonce. Ethernet setup
174
+ requests the code in the App UI. Business messages are AES-GCM protected.
175
+ - Each IP session has separate crypto state. Devices cap simultaneous sessions
176
+ at four; pending handshakes expire after two minutes, sessions after ten.
177
+ An encrypted `pod-credential {"op":"close"}` releases that temporary session.
178
+ - Stop init advertisement on durable completion. The existing session has a
179
+ short finish window for confirmation; normal business disconnection never
180
+ returns a completed device to initialization.
153
181
 
154
182
  ## 4. Pod state machine
155
183
 
156
184
  ```text
157
- unprovisioned ──connect──▶ session_open ──handshake──▶ secured
158
- secured ──prov-config apply──▶ wifi_joining ──▶ wifi_connected | wifi_failed
159
- wifi_failed ──retry──▶ wifi_joining
160
- wifi_connected ──deliver──▶ activating ──time sync, refresh, commit──▶ commissioned | failed
161
- any pre-commit state ──timeout/disconnect──▶ unprovisioned
185
+ no network -> BLE secure -> join -> save Wi-Fi -> network ready
186
+ Ethernet / saved Wi-Fi ------------------------> network ready
187
+ network ready -> advertise init -> LAN secure -> authorize -> activate -> save identity
188
+ join failure -> end attempt; start Wi-Fi setup again
189
+ activation failure -> end attempt; keep network and init advertisement
190
+ pairing expiry -> discard temporary sessions/code; keep network; accept a new code
162
191
  ```
163
192
 
193
+ The App reports `discovering_lan` and `connecting_lan` during BLE handoff.
194
+ Discovery snapshots distinguish `scanning` (BLE), `lanBrowsing`, optional
195
+ `lanError`, and candidate `transport` (`ble` or `lan`). LAN candidates carry the
196
+ full `deviceId`; Bluetooth denial never blocks them. The App retries initialization
197
+ by selecting the LAN candidate, possibly re-entering the current code. It first
198
+ reads actual device state: `commissioned` completes immediately, `activating`
199
+ waits for the existing attempt, and a failed attempt may issue afresh.
200
+
164
201
  Runtime states after commit:
165
202
 
166
203
  - `commissioned`: normal operation.
@@ -316,7 +353,7 @@ and unexpired pending pods, each
316
353
 
317
354
  All targets are handled by the native Client and never forwarded. Payloads are
318
355
  domain input JSON. Clients without a usable Bluetooth adapter report
319
- `adapter.state` and reject commissioning with `bluetooth_unavailable`.
356
+ `adapter.state` and reject BLE commissioning with `bluetooth_unavailable`; LAN candidates remain usable.
320
357
 
321
358
  A rejected request uses the facade error envelope
322
359
  `{"error","message","details":{"reason"}}`: `error` is `BAD_REQUEST` for an
@@ -394,9 +431,10 @@ example `storage_failed`) in `detail`. UIs show such a session as changing the
394
431
  pod's Wi-Fi rather than adding a pod.
395
432
 
396
433
  Session states, in order: `connecting`, `awaiting_code`, `securing`,
397
- `reading_info`, `awaiting_wifi`, `joining_wifi`, `awaiting_authorization`,
434
+ `reading_info`, `awaiting_wifi`, `joining_wifi`, `discovering_lan`,
435
+ `connecting_lan`, `awaiting_authorization`,
398
436
  `issuing`, `delivering`, `activating`, `completed`; terminal `failed`,
399
- `timed_out`. Wi-Fi states are skipped when the pod reports a
437
+ `timed_out`. LAN candidates skip all Wi-Fi states. Wi-Fi states are also skipped when the pod reports a
400
438
  configured network and `prov-config` status reports it connected; while that
401
439
  status is `connecting` the commissioner keeps polling, and only after a failure
402
440
  does it reset Wi-Fi (`prov-ctrl`) and offer new credentials. The commissioner
@@ -407,10 +445,11 @@ scan runs waits for it. `revision` increases on every observable change.
407
445
  `delivering` on it is `activateBefore` plus 30 seconds.
408
446
 
409
447
  Only the device's durable `commissioned` response confirms successful setup.
410
- Lost BLE delivery confirmation or activation status fails the attempt, even
448
+ Lost LAN delivery confirmation or activation status fails the attempt, even
411
449
  when Lion has already activated its credential. Expiry fails or times out the
412
450
  attempt. The commissioner does not recover interrupted setup through Lion's
413
- credential status. The user resets the device and starts a fresh attempt.
451
+ credential status. The user selects the network-ready device again, establishes
452
+ a fresh LAN session and reads its actual activation status.
414
453
 
415
454
  A refused `deliver` fails with `delivery_failed` carrying the pod's `reason`
416
455
  (`invalid_payload` or `wrong_state`) in `detail`. When the pod
@@ -423,14 +462,13 @@ app is in the background (Android foreground service of type
423
462
 
424
463
  Session error codes: `bluetooth_unavailable`, `device_busy`, `disconnected`,
425
464
  `code_rejected` (retry allowed), `code_locked`, `wifi_auth_failed`,
426
- `wifi_not_found`, `wifi_failed`, `issue_failed`, `delivery_failed`,
465
+ `wifi_not_found`, `wifi_failed`, `lan_unavailable`, `device_mismatch`, `issue_failed`, `delivery_failed`,
427
466
  `activation_failed` (with the pod's activation code, or `not_activated`, in
428
467
  `detail`), `session_timeout`. `code_locked` is terminal: the device shows a new
429
468
  code once it accepts handshakes again. `failed` and `timed_out` always carry `error`. A rejected
430
- code returns to `awaiting_code` with `code_rejected`; a failed join returns to
431
- `awaiting_wifi` with the `wifi_*` code. No other state carries `error`. Terminal states release the link and radio
432
- without revoking credentials. Reset the Pod and start over after an uncertain
433
- activation outcome; unused pending cloud credentials expire automatically.
469
+ code returns to `awaiting_code` with `code_rejected`; a failed Pod join ends in
470
+ `failed` with the `wifi_*` code. No other state carries `error`. Terminal states release the link and radio
471
+ without revoking credentials. Retry over LAN after an uncertain activation outcome; unused pending cloud credentials expire automatically.
434
472
 
435
473
  The schemas are `schema/pod-discovery.v1.schema.json` and
436
474
  `schema/pod-commission.v1.schema.json`.
@@ -5,6 +5,7 @@
5
5
  "candidate": {
6
6
  "candidateId": "ble:5E6F7A8B",
7
7
  "kind": "host",
8
+ "transport": "ble",
8
9
  "name": "Kitchen Host",
9
10
  "shortId": "5E6F7A8B",
10
11
  "rssi": -61,
@@ -5,6 +5,7 @@
5
5
  "candidate": {
6
6
  "candidateId": "ble:1A2B3C4D",
7
7
  "kind": "pod",
8
+ "transport": "ble",
8
9
  "name": "MeowPod S1",
9
10
  "model": "S1",
10
11
  "shortId": "1A2B3C4D",
@@ -5,6 +5,7 @@
5
5
  "candidate": {
6
6
  "candidateId": "ble:1A2B3C4D",
7
7
  "kind": "pod",
8
+ "transport": "ble",
8
9
  "name": "MeowPod S1",
9
10
  "model": "S1",
10
11
  "shortId": "1A2B3C4D",
@@ -5,6 +5,7 @@
5
5
  "candidate": {
6
6
  "candidateId": "ble:1A2B3C4D",
7
7
  "kind": "pod",
8
+ "transport": "ble",
8
9
  "name": "MeowPod S1",
9
10
  "model": "S1",
10
11
  "shortId": "1A2B3C4D",
@@ -1,11 +1,13 @@
1
1
  {
2
2
  "revision": 4,
3
+ "lanBrowsing": false,
3
4
  "scanning": true,
4
5
  "adapter": { "state": "ready" },
5
6
  "candidates": [
6
7
  {
7
8
  "candidateId": "ble:1A2B3C4D",
8
9
  "kind": "pod",
10
+ "transport": "ble",
9
11
  "name": "MeowPod S1",
10
12
  "model": "S1",
11
13
  "shortId": "1A2B3C4D",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.app-facade.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "callSchema": "schema/facade-call.v1.schema.json",
5
5
  "routing": "manifest/routing.v1.json",
6
6
  "routingSchema": "schema/routing.v1.schema.json",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.host-provision.targets.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "contract": "contract/host-provisioning-v1.md",
5
5
  "localTargets": [
6
6
  "/local/host/provision/start",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.hub.targets.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "appTargets": [
5
5
  "/app/hub/get",
6
6
  "/app/hub/remove",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.pod.targets.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "contract": "contract/pod-commissioning-v1.md",
5
5
  "localTargets": [
6
6
  "/local/pod/discovery/start",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.app-facade.routing.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "targetPrefix": "/app/",
5
5
  "default": {
6
6
  "execution": "scopeMode",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest": "mhome.messaging.targets.v1",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "requestTargets": [
5
5
  "/app/messaging/provider/list",
6
6
  "/app/messaging/provider-account/list",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mhome/app-facade-protocol",
3
- "version": "1.40.1",
3
+ "version": "1.41.0",
4
4
  "description": "Build-time protocol artifacts for mhome.app-facade",
5
5
  "license": "MIT",
6
6
  "repository": {
package/protocol.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "protocol": "mhome.app-facade",
3
- "contractVersion": "1.40.1",
3
+ "contractVersion": "1.41.0",
4
4
  "manifest": "manifest/app-facade.v1.json",
5
5
  "schemas": {
6
6
  "capabilityDescriptor": "schema/capability-descriptor.v1.schema.json",
@@ -204,7 +204,7 @@
204
204
  "type": "array",
205
205
  "items": {
206
206
  "type": "string",
207
- "pattern": "^(person|cluster):.+$"
207
+ "pattern": "^h:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
208
208
  }
209
209
  }
210
210
  },
@@ -16,6 +16,8 @@
16
16
  "reading_info",
17
17
  "awaiting_wifi",
18
18
  "joining_wifi",
19
+ "discovering_lan",
20
+ "connecting_lan",
19
21
  "awaiting_authorization",
20
22
  "issuing",
21
23
  "delivering",
@@ -86,6 +88,8 @@
86
88
  "bluetooth_unavailable",
87
89
  "device_busy",
88
90
  "disconnected",
91
+ "lan_unavailable",
92
+ "device_mismatch",
89
93
  "code_rejected",
90
94
  "code_locked",
91
95
  "wifi_auth_failed",
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://schemas.mhome.ai/app-facade/pod-discovery.v1.schema.json",
4
- "title": "Native-client BLE discovery snapshot (pods and Hosts)",
4
+ "title": "Native-client BLE and LAN discovery snapshot (pods and Hosts)",
5
5
  "type": "object",
6
6
  "additionalProperties": false,
7
- "required": ["revision", "scanning", "adapter", "candidates"],
7
+ "required": ["revision", "scanning", "lanBrowsing", "adapter", "candidates"],
8
8
  "properties": {
9
9
  "revision": { "type": "integer", "minimum": 0 },
10
10
  "scanning": { "type": "boolean" },
11
+ "lanBrowsing": { "type": "boolean" },
12
+ "lanError": { "type": "string" },
11
13
  "adapter": { "$ref": "#/$defs/adapter" },
12
14
  "candidates": { "type": "array", "items": { "$ref": "#/$defs/candidate" } }
13
15
  },
@@ -27,6 +29,7 @@
27
29
  "required": [
28
30
  "candidateId",
29
31
  "kind",
32
+ "transport",
30
33
  "name",
31
34
  "shortId",
32
35
  "rssi",
@@ -37,9 +40,11 @@
37
40
  "properties": {
38
41
  "candidateId": { "type": "string", "minLength": 1 },
39
42
  "kind": { "enum": ["pod", "host"] },
43
+ "transport": { "enum": ["ble", "lan"] },
44
+ "deviceId": { "type": "string", "minLength": 1, "maxLength": 64 },
40
45
  "name": { "type": "string", "minLength": 1 },
41
46
  "model": { "type": "string" },
42
- "shortId": { "type": "string", "pattern": "^[0-9A-F]{8}$" },
47
+ "shortId": { "type": "string", "minLength": 1, "maxLength": 64 },
43
48
  "rssi": { "type": "integer" },
44
49
  "commissionable": { "type": "boolean" },
45
50
  "wifiConfigured": { "type": "boolean" },