@mhome/app-facade-protocol 1.42.2 → 1.44.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.
package/README.md CHANGED
@@ -46,7 +46,7 @@ requests carry a stable `details.reason`. See
46
46
  [the contract](contract/pod-commissioning-v1.md). Targets, BLE constants, session
47
47
  timings and request reasons are in `manifest/pod-targets.v1.json`; snapshots use
48
48
  `schema/pod-discovery.v1.schema.json` and `schema/pod-commission.v1.schema.json`;
49
- the owner's credential status is `schema/pod-credential-status.v1.schema.json`.
49
+ User Pod enrollment uses a one-time login approval; activation is confirmed on the paired device.
50
50
 
51
51
  `host_provision` (1.34.0) defines BLE Wi-Fi provisioning of embedded Hosts
52
52
  (`/local/host/provision/*`) on the same BLE layer; discovery candidates carry
@@ -69,23 +69,21 @@ Host inventory and management on the selected Hub through `/app/system/hosts/*`.
69
69
  A native Client remains the path when no local Hub is selected. Lion's legacy
70
70
  controller annotations and non-`/app/*` protocols are outside this contract.
71
71
 
72
- ### System plugin catalog
72
+ ### Plugin catalog
73
73
 
74
- `/app/plugin/catalog/list` returns `manifest/plugin-catalog.v1.json` from Cloud,
75
- without requiring a local Space or connected Hub. Authentication and Space
76
- membership still apply. Core also serves this catalog for clients with an older
77
- routing manifest. The response schema is `schema/plugin-catalog.v1.schema.json`.
74
+ `/app/plugin/catalog/list` reads publisher-signed platform indexes under
75
+ `https://install.mhome.ai/plugins/stable/<platform>/catalog.bundle.json`.
76
+ Cloud serves this browsing view without a connected Hub. Authentication and
77
+ Space membership still apply. Core exposes the same response for local callers.
78
+ The response schema is `schema/plugin-catalog.v1.schema.json`; `platformErrors`
79
+ reports unavailable indexes while preserving results from other platforms.
78
80
 
79
- The catalog describes supported types, not installed software or bindings.
80
- `componentId` joins to Host runtime inspection; `nodeType` joins to existing
81
- Plugin candidate and installed/binding queries. Platform installability and
82
- versions come from the Host release catalog, not this directory.
83
-
84
- Core and Cloud embed generated copies. After editing the catalog, run
85
- `node scripts/sync-plugin-catalog.mjs --write <core-resource> <cloud-resource>`
86
- from the Foundation root; omit `--write` to check conformance. Resource paths are
87
- `src/application/plugin-catalog.v1.json` (Core) and
88
- `src/main/resources/app/plugin-catalog.v1.json` (Cloud module-service).
81
+ Foundation defines contracts, not the list of Plugins. Plugin source owns its
82
+ definition; releases publish it with each immutable per-version manifest.
83
+ `componentId` joins Host inventory, and `nodeType` joins candidates and bindings.
84
+ Catalog availability is a browsing hint. Binding uses `/node/metadata` from the
85
+ actual running Node, verifies publisher evidence, and persists that definition
86
+ with the binding. A cached latest catalog never overrides installed facts.
89
87
 
90
88
  ## Shared messaging codes (1.10)
91
89
 
@@ -94,7 +92,8 @@ Code creation may omit bot/account and group/surface identifiers. The receiving
94
92
  ## Space runtime observations
95
93
 
96
94
  `/app/system/instances/get` reads Space instances; `/app/system/clients/get` reads
97
- client sessions; `/app/system/hosts/get`, `/app/system/hosts/runtime`, and
98
- `/app/system/hosts/claim` are Hub-vantage Host inventory, management, and
99
- first-claim. These remain Hub-owned with cloud relay and Space membership. See
100
- [SYSTEM.md](SYSTEM.md).
95
+ Client sessions. Host inventory and operations use `/app/system/hosts/get` and
96
+ `/app/system/hosts/runtime`, forwarding cloud-issued User tokens through the
97
+ current Core. First claim is direct LAN only. See [SYSTEM.md](SYSTEM.md).
98
+
99
+ Business operation permissions are declared once in [the authorization manifest](manifest/authorization.v1.json), consumed by Core and Lion. See [business authorization](contract/authorization-v1.md).
@@ -134,18 +134,17 @@ Session states, in order: `connecting`, `awaiting_code`, `securing`,
134
134
  (IPv4 only, possibly empty) and `claimToken` from the `finish` response; the
135
135
  commissioner sends `finish` (up to 3 times while the secure session lasts)
136
136
  before reporting it. `claimToken` is absent when `finish` could not be
137
- delivered, in a re-provision window, and with Hosts that predate claim tokens.
137
+ delivered, in a re-provision window.
138
138
  No other state carries `claimToken`.
139
139
 
140
- The first claim (TOFU) of a Host that was provisioned over BLE must present this
141
- token: `/app/system/hosts/claim` takes `{"hostId","claimToken"?}` and the native
142
- `host.claim` passes it through to the Host's offline claim, `POST /v1/auth/claim`
143
- `{"proof","claimToken"?}` (`core-api` `host::auth::ClaimRequest`). Clients send
144
- `claimToken` only when they have one, because older Hosts reject unknown fields.
145
- While a token is outstanding the Host answers a claim without it, or with a
146
- different one, with 403 `{"code":"HOST_CLAIM_TOKEN_REQUIRED"}`. The token stays valid until a claim succeeds or the Host is
147
- factory reset; the UI claims automatically with it after `completed`. Hosts
148
- that were never provisioned over BLE keep the plain first claim.
140
+ The first claim (TOFU) after BLE provisioning must include the returned token.
141
+ Native `host.claim` passes it to `POST /v1/auth/claim`, alongside the environment,
142
+ cloud proof and direct Client exchange secret (`core-api::host::access::ClaimRequest`).
143
+ There is no Core-proxied first-claim route. The Host rejects a missing or incorrect
144
+ provisioning token with 403 `HOST_CLAIM_TOKEN_REQUIRED`. The token remains valid
145
+ until claim succeeds or factory reset. The App claims automatically after
146
+ `completed`. Hosts that were not provisioned over BLE use the ordinary direct
147
+ LAN claim flow. See the Foundation Host management authorization contract.
149
148
 
150
149
  Error codes are the pod session codes without the credential ones:
151
150
  `bluetooth_unavailable`, `device_busy`, `disconnected`, `code_rejected` (retry
@@ -130,7 +130,7 @@ the response also carries `"mode":"reprovision"` and `wifiConfigured` is false
130
130
 
131
131
  `pod-credential` requests:
132
132
 
133
- - `{"op":"deliver","clientMode":"user","podId","refreshToken","scopeId","cloudApi","cloudWs"}` →
133
+ - `{"op":"deliver","clientMode":"user","podId","loginGrant","scopeId","cloudApi","cloudWs"}` →
134
134
  `{"ok":true,"state":"activating"}`. Accepted only in `wifi_connected` outside a
135
135
  Wi-Fi change window; otherwise `{"ok":false,"state":<current pod state>,"reason"}`
136
136
  with `reason` `invalid_payload` (a missing or malformed field, including a
@@ -220,136 +220,51 @@ Runtime states after commit:
220
220
  identity key and cached Hub tokens, reboot to `unprovisioned`. No cloud
221
221
  request is needed. Logout also clears local credentials without cloud revocation.
222
222
 
223
- Time sync uses SNTP; if NTP is unreachable the pod may use the `Date` header of a
224
- Lion HTTPS response. Activation proofs require a synced clock.
225
-
226
- ## 5. Lion credential API
227
-
228
- All bodies are JSON. Errors use the standard Lion envelope
229
- `{"error","message","details"?}`. Credential failures are `UNAUTHORIZED` with one
230
- of these `message` values: `pod_credential_invalid` (unknown pod, wrong or
231
- revoked refresh token, or an owner account that no longer exists),
232
- `pod_proof_invalid`, `pod_proof_replayed`, `pod_activation_expired`. A pod treats
233
- `pod_credential_invalid` and `pod_activation_expired` as revocation. Renaming a
234
- pod the caller does not own, or that does not exist, is `NOT_FOUND` with
235
- `pod_not_found`.
236
-
237
- ### Issue
238
-
239
- `POST /api/v1/pod/credential/issue`, user `Authorization`, `ScopeId` header.
240
-
241
- ```json
242
- {
243
- "deviceId": "a0b1c2d3e4f5",
244
- "model": "S1",
245
- "platform": "esp32c5",
246
- "firmwareVersion": "0.3.0",
247
- "name": "MeowPod S1",
248
- "identity": { "keyId": "p256:…", "alg": "ES256", "x": "…", "y": "…", "fingerprint": "…" },
249
- "clientId": "issuing client id"
250
- }
251
- ```
252
-
253
- `deviceId` matches `[A-Za-z0-9._:-]{1,64}`; `model` and `name` are 1–64
254
- characters, `platform` and `firmwareVersion` at most 64, `clientId` at most 128. Lion requires Space membership for `ScopeId`, validates the identity, and
255
- creates a `pending` credential that must activate before `activateBefore`
256
- (issue time + 10 minutes). Response `{"podId","refreshToken","activateBefore"}`.
257
-
258
- ### Refresh and activation
259
-
260
- `POST /api/v1/pod/token/refresh`, body `{"podId","refreshToken","proof"}`.
261
-
262
- `proof` is a compact ES256 JWS signed by the identity key: header `kid=keyId`;
263
- claims `iss` and `sub` = `podId`, a single-valued `aud` = `pod-token-refresh`,
264
- `token_hash` = unpadded base64url SHA-256 of the UTF-8 refresh token (including
265
- `pod-`), `iat`, `exp` with `0 ≤ exp - iat ≤ 120 s`, and a unique `jti` of at most
266
- 128 characters. Lion allows 120 s clock skew and remembers each `jti` for
267
- 7 minutes (proof lifetime plus twice the skew, plus a minute), longer than any
268
- proof can be accepted; a reused `jti` in that window fails with
269
- `pod_proof_replayed`.
270
-
271
- Before the first successful refresh moves `pending` to `active`, Lion deletes every other
272
- credential with the same `identity.keyId` (whichever user holds it) and the same
273
- user's other credentials with the same `deviceId`. A `pending` credential past
274
- `activateBefore` is rejected and deleted. Revoked credentials are deleted, so a
275
- later refresh fails with `pod_credential_invalid`. Response
276
- `{"accessToken","expiresIn"}`.
277
-
278
- ### Status
279
-
280
- `POST /api/v1/pod/credential/status`, user `Authorization`, body `{"podId"}` →
281
- `{"podId","status","activatedAt"?}` with `status` one of `pending`, `active`,
282
- `absent` (schema `schema/pod-credential-status.v1.schema.json`). `absent`
283
- covers no such credential, revoked, pending past `activateBefore`, and a
284
- credential owned by another user, so the answer never reveals other users'
285
- pods. `activatedAt` (epoch ms) is present when `active`. It is informational;
286
- commissioning success requires the device confirmation described in section 7.
287
-
288
- ### Reset and cleanup
289
-
290
- There is no public Pod credential revocation endpoint and no App action that
291
- withdraws a Pod's access without contacting the device. Factory reset executes
292
- on the Pod itself. Remote reset, when provided, must reach an online Pod; a
293
- cloud-side deletion is not a successful reset. This contract currently exposes
294
- no remote reset target. Each setup creates a new Pod credential.
295
-
296
- ### List and rename
297
-
298
- `POST /api/v1/pod/list` (user) returns `{"pods":[…]}`, the user's active pods
299
- and unexpired pending pods, each
300
- `{"podId","name","deviceId","model","platform","firmwareVersion","identityFingerprint","status","issuedAt","activatedAt"}`.
301
- `POST /api/v1/pod/rename` `{"podId","name"}` (user) returns the updated pod.
302
-
303
- ### Tokens
304
-
305
- - Access: `pod-` + HS256 JWT. Claims: `iss`, `aud`, `sub` = `podId`, `user_id`,
306
- `tenant_id`, `device_id`, `token_type` = `pod_access`, `iat`, `exp` (3600 s),
307
- `jti`. The signing key is derived from the Hub secret with HMAC-SHA256 over the
308
- label `mhome-pod-access-v1`, so Hub and pod tokens never verify as each other.
309
- - Refresh: `pod-` + 43-character base64url of 32 random bytes. Lion stores only
310
- its SHA-256. It never rotates; the identity proof binds it to the device.
311
- - Pods send `Authorization: Bearer pod-…` (and `"token":"Bearer pod-…"` on the
312
- WebSocket). `pod-` is dispatched by prefix before any JWT parsing. Only the cloud WebSocket
313
- `/auth` and `/api/v1/hub/token/exchange` accept pod access tokens. Every other
314
- API keeps accepting user tokens only. No target allowlist applies in v1.
315
- - Lion checks an access token against the stored credential on every use, so a
316
- revoked or superseded credential stops working before its `exp`.
317
- - A pod session acts as its user across all of that user's Spaces. A cloud
318
- WebSocket session authenticated with a pod access token remembers the `podId`
319
- and the token `exp`. Lion closes it with a policy-violation close whose reason
320
- is `POD_TOKEN_EXPIRED` at `exp` and `POD_REVOKED` when the pod is revoked;
321
- revocations reach sessions on every Lion instance through a shared event
322
- channel. Either close only ends that connection; whether the pod is revoked
323
- is decided by its next refresh.
324
- - `/api/v1/hub/token/exchange` with a pod access token returns a Hub JWT with
325
- `clientKind` = `pod` and `podId` claims and the same lifetime as a Hub JWT
326
- for a user token.
327
- - Pod and ordinary Client local Hub JWTs have the same 100-year lifetime
328
- (100 * 365 days). There is no per-Pod Hub revocation list or cloud-to-Hub
329
- revocation sync. Local tokens are cleared on the Pod by reset/logout.
223
+ Linux uses its OS time service and ESP uses background SNTP. User login proofs
224
+ require usable UTC and cloud TLS retains normal CA and date validation. Space
225
+ Client local authentication pins the enrolled Core identity and signs a fresh
226
+ nonce; Core checks credential expiry with its own clock. HTTP `Date` is diagnostic
227
+ and does not override the system clock or relax TLS validation.
228
+
229
+ ## 5. User Pod login approval
230
+
231
+ `POST /api/v1/pod/login/approve` uses the assisting App's ordinary User
232
+ Authorization. Its body contains `deviceId`, `model`, `platform`,
233
+ `firmwareVersion`, `name`, `identity` (P-256 keyId, alg, x, y, fingerprint), and
234
+ optional `clientId`. It has no Space authorization header or issued-Space scope.
235
+ Lion returns `{podId, loginGrant, activateBefore}`. The random approval expires
236
+ in ten minutes and is bound to this User and the Pod identity key. It is not a
237
+ login refresh token. The helper must never deliver its own login credentials.
238
+
239
+ The App delivers the approval through the paired LAN channel. `scopeId` in the
240
+ device delivery selects the initial UI Space; it does not constrain the User
241
+ identity or grant Space authority. The device independently redeems the approval
242
+ at `POST /api/v1/pod/login/redeem` with `{podId, loginGrant, proof}`. The proof is
243
+ signed by the paired device's key with audience `user-device-login` and binds the
244
+ approval. Lion coordinates Cognito CUSTOM_AUTH to create the device's own
245
+ ordinary Cognito session. Approval redemption is single-use. Only the device
246
+ stores the resulting refresh token, and refresh uses the standard User login
247
+ endpoint. A failed attempt is retried with a new approval; no old protocol is
248
+ accepted.
249
+
250
+ `podId` identifies the enrollment/device-list entry, not the authenticated User.
251
+ `POST /api/v1/pod/list` and `/api/v1/pod/rename` use the ordinary User login.
252
+ There is no cloud credential-status polling or server-side Pod revocation flow.
253
+ Factory reset clears device credentials and rotates the pairing identity.
330
254
 
331
255
  ## 6. Pod runtime authentication
332
256
 
333
- - Cloud `/auth`: `{"token":"Bearer pod-…","devToken":null,"source":"pod","deviceId","activeScope"}`,
334
- then `/focus`. Lion rejects `source=pod` without a pod token, a pod token
335
- with any other source, and a `deviceId` other than the one the credential was
336
- issued to.
337
- - The pod refreshes its access token proactively, 2 minutes before `exp`, and
338
- re-authenticates the cloud session with the new token. A refresh rejected
339
- with `pod_credential_invalid` or `pod_activation_expired` moves the pod to
340
- `needs_recommission`.
341
- - Space switching: the pod chooses among the Spaces returned by `/auth` and
342
- re-sends `/focus`; it keeps one Hub JWT and Hub key per Hub and Space.
343
- - Local Hub: the Hub identity endpoint `POST /api/v1/hub/identity/get`
344
- (`{"hubId","tenantId","scopeId"}`) is public and needs no token. The pod
345
- proves the Hub identity, then authenticates to the local Hub the way a Client
346
- does: the first local `/auth` carries its pod access token with
347
- `source=cloud` (`clientSource=pod`, `deviceId`, `tenantId`, `scopeId`,
348
- `hubId`). The Hub exchanges that token with the cloud and returns a
349
- long-lived local Hub JWT in `jwtToken`, which the pod stores per Hub. Later
350
- local `/auth` requests use only that JWT with `source=local`. When the stored
351
- JWT is about to expire or the Hub rejects it, the pod discards it and
352
- authenticates with `source=cloud` again.
257
+ User Pods use ordinary Cognito User authentication for cloud business traffic.
258
+ Local Core connections use the same Lion-issued local credentials and Core
259
+ verification as other User Clients. Each credential retains the actual User
260
+ principal; initial Space selection is not embedded in a special Pod login token.
261
+ Space Pods use only their enrolled local Core and their Space credential; they
262
+ receive no User login grant or Cognito session. See section 8 for Space enrollment.
263
+
264
+ Setup succeeds only after the paired device reports `commissioned`, meaning its
265
+ business connection has authenticated. Cloud approval issuance alone is not
266
+ activation. Reset clears the local credentials; an unreachable device is not
267
+ considered reset.
353
268
 
354
269
  ## 7. Native Client targets
355
270
 
@@ -447,11 +362,12 @@ scan runs waits for it. `revision` increases on every observable change.
447
362
  `delivering` on it is `activateBefore` plus 30 seconds.
448
363
 
449
364
  Only the device's durable `commissioned` response confirms successful setup.
450
- Lost LAN delivery confirmation or activation status fails the attempt, even
451
- when Lion has already activated its credential. Expiry fails or times out the
452
- attempt. The commissioner does not recover interrupted setup through Lion's
453
- credential status. The user selects the network-ready device again, establishes
454
- a fresh LAN session and reads its actual activation status.
365
+ Lost LAN confirmation triggers at most three reconnections to this attempt's
366
+ endpoint. Each connection reestablishes channel security and checks device,
367
+ environment and enrollment identity before reading status. Never issue or
368
+ deliver a second credential during recovery. Failure to confirm, or expiry,
369
+ ends the attempt; the user may then select the network-ready device again.
370
+ Cloud state alone never confirms successful device setup.
455
371
 
456
372
  A refused `deliver` fails with `delivery_failed` carrying the pod's `reason`
457
373
  (`invalid_payload` or `wrong_state`) in `detail`. When the pod
@@ -480,7 +396,7 @@ The schemas are `schema/pod-discovery.v1.schema.json` and
480
396
 
481
397
  `POST /api/v1/pod/space/credential/issue` uses the assisting App's user
482
398
  `Authorization` and `ScopeId`, with the same device identity request as personal
483
- issuance. Any current member may assist. The Space must be local and have one
399
+ issuance. Only the current Space owner may assist. The Space must be local and have one
484
400
  published Hub with business and Host TLS identities. Cloud Spaces must complete
485
401
  Hub setup first. The issuer creates a fresh `appClientId` (`a:<random id>`) and
486
402
  signs a local JWT using the existing Space signing key and local-client audience.