@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 +19 -20
- package/contract/host-provisioning-v1.md +9 -10
- package/contract/pod-commissioning-v1.md +50 -134
- package/fixtures/authorization/decisions.json +618 -0
- package/fixtures/pod-commission.awaiting-authorization.json +4 -2
- package/fixtures/pod-commission.reprovision-completed.json +2 -1
- package/fixtures/setup-cancel.request.json +9 -0
- package/fixtures/setup-cancel.response.json +11 -0
- package/fixtures/setup-submit.request.json +12 -0
- package/fixtures/setup-submit.response.json +11 -0
- package/manifest/app-facade.v1.json +18 -8
- package/manifest/host-provision-targets.v1.json +1 -1
- package/manifest/hub-targets.v1.json +1 -1
- package/manifest/pod-targets.v1.json +3 -6
- package/manifest/routing.v1.json +8 -4
- package/manifest/targets.v1.json +5 -1
- package/package.json +1 -1
- package/protocol.json +4 -2
- package/schema/host-access.v1.schema.json +33 -0
- package/schema/host-authorization.v1.schema.json +55 -0
- package/schema/messaging-frame.v3.schema.json +139 -2
- package/schema/pod-commission.v1.schema.json +267 -41
- package/schema/system-hosts.v1.schema.json +122 -1
- package/schema/pod-credential-status.v1.schema.json +0 -18
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
|
-
|
|
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
|
-
###
|
|
72
|
+
### Plugin catalog
|
|
73
73
|
|
|
74
|
-
`/app/plugin/catalog/list`
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
98
|
-
`/app/system/hosts/
|
|
99
|
-
|
|
100
|
-
|
|
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
|
|
137
|
+
delivered, in a re-provision window.
|
|
138
138
|
No other state carries `claimToken`.
|
|
139
139
|
|
|
140
|
-
The first claim (TOFU)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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","
|
|
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
|
-
|
|
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
|
-
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
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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.
|
|
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.
|