@pollen-robotics/reachy-mini-sdk 1.8.0 → 1.8.1

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.
Files changed (67) hide show
  1. package/APP_CREATION_GUIDE.md +1795 -0
  2. package/README.md +2 -5
  3. package/dist/animation/distance.d.ts +87 -0
  4. package/dist/animation/distance.d.ts.map +1 -0
  5. package/dist/animation/distance.js +140 -0
  6. package/dist/animation/distance.js.map +1 -0
  7. package/dist/animation/index.d.ts +34 -0
  8. package/dist/animation/index.d.ts.map +1 -0
  9. package/dist/animation/index.js +33 -0
  10. package/dist/animation/index.js.map +1 -0
  11. package/dist/animation/pose.d.ts +40 -0
  12. package/dist/animation/pose.d.ts.map +1 -0
  13. package/dist/animation/pose.js +15 -0
  14. package/dist/animation/pose.js.map +1 -0
  15. package/dist/animation/presets.d.ts +85 -0
  16. package/dist/animation/presets.d.ts.map +1 -0
  17. package/dist/animation/presets.js +82 -0
  18. package/dist/animation/presets.js.map +1 -0
  19. package/dist/animation/safe-return.d.ts +90 -0
  20. package/dist/animation/safe-return.d.ts.map +1 -0
  21. package/dist/animation/safe-return.js +123 -0
  22. package/dist/animation/safe-return.js.map +1 -0
  23. package/dist/lib/reachy-mini.d.ts +99 -0
  24. package/dist/lib/reachy-mini.d.ts.map +1 -1
  25. package/dist/lib/reachy-mini.js +314 -9
  26. package/dist/lib/reachy-mini.js.map +1 -1
  27. package/dist/lib/types.d.ts +27 -0
  28. package/dist/lib/types.d.ts.map +1 -1
  29. package/host/README.md +14 -12
  30. package/host/dist/ReachyHost.d.ts +2 -1
  31. package/host/dist/ReachyHost.d.ts.map +1 -1
  32. package/host/dist/chunks/index-lMs2sCXG.js +439 -0
  33. package/host/dist/chunks/{mountHost-D7cgkU9Q.js → mountHost-DjE5zE9R.js} +11142 -11014
  34. package/host/dist/chunks/{reachy-mini-B1hlBmDQ.js → reachy-mini-GHNS4GSp.js} +494 -307
  35. package/host/dist/components/PickerView.d.ts.map +1 -1
  36. package/host/dist/components/PostOAuthSplash.d.ts +25 -0
  37. package/host/dist/components/PostOAuthSplash.d.ts.map +1 -0
  38. package/host/dist/components/ReachyHostShell.d.ts.map +1 -1
  39. package/host/dist/components/TopBar.d.ts +8 -1
  40. package/host/dist/components/TopBar.d.ts.map +1 -1
  41. package/host/dist/components/WelcomeBackOverlay.d.ts.map +1 -1
  42. package/host/dist/embed/index.d.ts +3 -2
  43. package/host/dist/embed/index.d.ts.map +1 -1
  44. package/host/dist/entry/auto.js +2 -2
  45. package/host/dist/entry/embed.js +2 -2
  46. package/host/dist/hooks/useHostBridge.d.ts +6 -0
  47. package/host/dist/hooks/useHostBridge.d.ts.map +1 -1
  48. package/host/dist/hooks/useSdk.d.ts.map +1 -1
  49. package/host/dist/index.js +3 -3
  50. package/host/dist/lib/protocol.d.ts +17 -3
  51. package/host/dist/lib/protocol.d.ts.map +1 -1
  52. package/host/dist/lib/settings.d.ts +3 -2
  53. package/host/dist/lib/settings.d.ts.map +1 -1
  54. package/host/dist/mountHost.d.ts.map +1 -1
  55. package/host/dist/ui/design/IdentityChipBar.d.ts +46 -0
  56. package/host/dist/ui/design/IdentityChipBar.d.ts.map +1 -0
  57. package/host/dist/ui/design/LinkQualityBars.d.ts +32 -0
  58. package/host/dist/ui/design/LinkQualityBars.d.ts.map +1 -0
  59. package/host/dist/ui/design/MetaPill.d.ts +41 -0
  60. package/host/dist/ui/design/MetaPill.d.ts.map +1 -0
  61. package/host/dist/ui/design/TransportChip.d.ts +39 -0
  62. package/host/dist/ui/design/TransportChip.d.ts.map +1 -0
  63. package/package.json +7 -4
  64. package/CHANGELOG.md +0 -188
  65. package/host/APP_AUTHOR_GUIDE.md +0 -646
  66. package/host/SPEC.md +0 -618
  67. package/host/dist/chunks/index-CyLPysJS.js +0 -400
package/host/SPEC.md DELETED
@@ -1,618 +0,0 @@
1
- # Reachy Mini app boot spec
2
-
3
- | Spec version | Status | Last updated |
4
- |--------------|------------|--------------|
5
- | 1.0 | Active | 2026-05-16 |
6
-
7
- Source-of-truth for what a Reachy Mini app must do at boot, what
8
- `@pollen-robotics/reachy-mini-sdk/host` does for it, and the contract between the
9
- shell, the embed and the app code.
10
-
11
- This file describes **observable behaviour and invariants**. The
12
- wire format lives in [`src/lib/protocol.ts`](./src/lib/protocol.ts).
13
- The README is a tour for first-time integrators.
14
-
15
- > **Read this in 15 minutes**: §1-§5 are the integration contract,
16
- > §6 is the protocol, §7 is two state diagrams, §8 lists the
17
- > non-negotiable invariants. §9-§11 are appendices.
18
-
19
- ## Table of contents
20
-
21
- 1. [Roles](#1-roles)
22
- 2. [Two boot modes, one URL surface](#2-two-boot-modes-one-url-surface)
23
- 3. [Mode A: Hub standalone flow](#3-mode-a-hub-standalone-flow)
24
- 4. [Mode B: Mobile handoff flow](#4-mode-b-mobile-handoff-flow)
25
- 5. [App-author contract](#5-app-author-contract)
26
- 6. [Protocol v1](#6-protocol-v1)
27
- 7. [State machines](#7-state-machines)
28
- 8. [Engineering invariants](#8-engineering-invariants)
29
- 9. [Non-goals](#9-non-goals)
30
- 10. [Threat model](#10-threat-model)
31
- 11. [Backlog](#11-backlog)
32
-
33
- ## How to evolve this spec
34
-
35
- - §1-§7 describe the **current implementation**: update them as the
36
- code changes, in the same PR.
37
- - §8 lists **hard invariants**: every PR that touches the bridge or
38
- the FSM must keep them green or update them explicitly with a
39
- CHANGELOG entry.
40
- - §11 is the **backlog**: graduate items into §8 only when they are
41
- implemented and the cost of regressing them is real.
42
-
43
- ---
44
-
45
- ## 1. Roles
46
-
47
- Three actors, one app repository:
48
-
49
- | Actor | Lives in | Owns |
50
- |------------|-----------------------------------|----------------------------------------------------------------------------|
51
- | **App** | `index.html` + `src/dispatch.ts` | UI, app-specific UX, **and full freedom over framework / tooling choices** |
52
- | **Host** | `@pollen-robotics/reachy-mini-sdk/host/auto` (CDN) | OAuth, robot discovery, robot picker, connecting overlay, end-session flow |
53
- | **Embed** | `@pollen-robotics/reachy-mini-sdk/host/embed` (CDN) | SDK lifecycle inside the iframe (`startSession`, `ensureAwake`, teardown) |
54
-
55
- The **App** consumes:
56
-
57
- - the Reachy Mini SDK (loaded as a CDN module by `index.html`,
58
- exposed as `window.ReachyMini`),
59
- - the `@pollen-robotics/reachy-mini-sdk/host` package (npm dep for types, CDN entries
60
- at runtime).
61
-
62
- The App contains zero auth code, zero picker code, zero
63
- session-lifecycle code.
64
-
65
- ### App identity: `owner/space`
66
-
67
- A Reachy Mini app is **uniquely identified by its Hugging Face
68
- Space ID**, of the form `owner/space` (e.g. `pollen-robotics/emotions`).
69
- Everything downstream of identity flows from this single string:
70
-
71
- - The catalog API filters and dedupes apps by `space.id`.
72
- - The mobile app stores and recalls "last opened" apps by `owner/space`.
73
- - The host shell does **not** need this ID at runtime (it lives
74
- inside the app it renders); it's used solely by the discovery
75
- surface (catalog API + mobile).
76
-
77
- ### Official vs community apps
78
-
79
- An app is **official** if and only if its Space ID starts with
80
- `pollen-robotics/`. There is no allowlist, no separate registry,
81
- no `official: true` field anywhere. Adding `pollen-robotics/` to
82
- your URL is the entire qualification.
83
-
84
- Where the distinction surfaces:
85
-
86
- | Surface | Behaviour |
87
- |----------------------|-----------------------------------------------------------------|
88
- | Mobile catalog | "Official" badge / sort priority on `pollen-robotics/*` Spaces |
89
- | Website `/api/js-apps` | Returns `isOfficial: true` for `pollen-robotics/*` |
90
- | Host shell | **No notion of "official"**. Renders the app the same way always |
91
-
92
- This is a non-decision at the host level: the host doesn't know,
93
- doesn't care. Keep it that way.
94
-
95
- ### Tech freedom is a core design principle
96
-
97
- The iframe boundary is intentional: it lets an app pick **any
98
- framework, any bundler, any styling system** without the host
99
- caring. The reference apps prove this in practice:
100
-
101
- | Reference app | Stack | Demonstrates |
102
- |-------------------------------------|-----------------------------|-----------------------------------------------|
103
- | `reachy_mini_emotions` | React 19 + MUI 7 + Vite | A typed React app with rich UI components |
104
- | `reachy_mini_telepresence` | React 19 + MUI 7 + Vite | An app that uses media streams + camera UI |
105
- | `reachy_mini_minimal_conversation` | **Vanilla TS + Vite** | Zero-framework app, ~few KB runtime overhead |
106
-
107
- If you add a new app, you do **not** need to consult the host
108
- team about your stack. The only contract is: render in the
109
- iframe, talk to the host through `connectToHost()`, ship the 4
110
- files in §5.
111
-
112
- ### Why React + MUI for the host (and only the host)
113
-
114
- The host shell (`@pollen-robotics/reachy-mini-sdk/host/auto`) is built with **React +
115
- MUI + Emotion**. This is an explicit, assumed choice:
116
-
117
- - The shell needs a real component library: forms (sign-in),
118
- lists (picker), overlays (connecting / leaving), top bar, dark
119
- mode toggles. Hand-rolling these in vanilla would slow us down
120
- with no benefit to app authors.
121
- - The shell renders only **outside the iframe** and only between
122
- sessions; once an app is live, the shell's bundle is idle.
123
- - App authors who chose a different framework still load the
124
- shell React+MUI bundle for sign-in/picker UI. This is the cost
125
- of the iframe model and we accept it (see §9 Non-goals).
126
-
127
- The trade-off favours **fast host iteration + tech freedom for
128
- apps** over a slimmer host shell. A vanilla micro-shell remains
129
- possible if usage data ever justifies it (§11 backlog).
130
-
131
- ---
132
-
133
- ## 2. Two boot modes, one URL surface
134
-
135
- The same `index.html` is served for both modes. The dispatcher
136
- (`src/dispatch.ts`) picks between them based on the URL.
137
-
138
- | Mode | URL shape | What happens |
139
- |-----------------------|----------------------------------------------------------------------|------------------------------------------------------|
140
- | **A. Hub standalone** | `https://<space>.hf.space/` | Full host shell (OAuth → picker → iframe with app) |
141
- | **B. Mobile handoff** | `https://<space>.hf.space/?embedded=1#creds=<base64(CredsBundle)>` | Skip shell; app boots directly, creds come via hash |
142
-
143
- ### Dispatch rule
144
-
145
- ```
146
- if (URL.searchParams.has("embedded") && URL.hash.startsWith("#creds=")):
147
- boot embed -> import("./embed")
148
- else:
149
- boot host -> import("@pollen-robotics/reachy-mini-sdk/host/auto").mountHost({...})
150
- ```
151
-
152
- `?embedded=1` without creds is an invalid mode - render `ErrorView`.
153
-
154
- ### `CredsBundle` (lives only in the URL hash, never in search)
155
-
156
- The full shape lives in `src/lib/protocol.ts` (`CredsBundle`).
157
- Key field: `hfToken` is a short-lived HF bearer (see §8.3). The
158
- hash is never sent to a server; the embed wipes it with
159
- `history.replaceState` on the very first synchronous tick, before
160
- any `await`.
161
-
162
- ---
163
-
164
- ## 3. Mode A: Hub standalone flow
165
-
166
- The user lands on `https://<space>.hf.space/` from anywhere
167
- (direct link, HF Space gallery, etc.).
168
-
169
- ### 3.1 Boot → auth → picker
170
-
171
- Phase machine inside `useReachyHost`:
172
-
173
- ```
174
- booting → (signed-out | authenticated) → connecting → connected → picking
175
- ```
176
-
177
- - **`booting`**: wait for `window.ReachyMini`, instantiate the SDK,
178
- call `instance.authenticate()`.
179
- - **`signed-out`**: render `SignInView` with a "Continue with
180
- Hugging Face" button that starts an OAuth redirect.
181
- - **`authenticated`** → `connecting` → `connected` (SSE welcome) →
182
- `picking`.
183
-
184
- During `picking`, `RobotPickerView` listens to the SDK's
185
- `robotsChanged` event so robots appearing / disappearing / going
186
- busy are reflected live.
187
-
188
- The picker auto-picks only when `instance.preselectedRobotId` is
189
- set (Mode B only). In standalone mode we always show the picker,
190
- even with a single robot online.
191
-
192
- ### 3.2 OAuth "welcome back" animation
193
-
194
- If the boot was triggered by an OAuth callback
195
- (`consumeOAuthPending()` returns true), once we land on
196
- `authenticated` with a known username, overlay `WelcomeBackView`
197
- on top of the picker. A normal tab re-open (no redirect) does NOT
198
- show the animation.
199
-
200
- ### 3.3 User clicks a robot → handing off
201
-
202
- 1. `selectRobot(robotId)` → phase `handing-off`.
203
- 2. `ReachyHost.tsx` mounts `<AppFrame>` with the iframe pointing
204
- at `<same-origin>?embedded=1#creds=<base64>`.
205
- 3. The iframe boots through `index.html` again, taking the embed
206
- branch.
207
- 4. `ConnectingView` is overlaid on top of the iframe (3-step
208
- stepper), driven by the embed's `embed:app-state` messages.
209
-
210
- ### 3.4 ConnectingView (3 steps)
211
-
212
- | Step | Internal name | Embed work |
213
- |-----------|---------------|-----------------------------------------|
214
- | Step 1 | `link` | `reachy.connect()` (open SSE) |
215
- | Step 2 | `session` | `reachy.startSession(robotPeerId)` |
216
- | Step 3 | `wake` | `reachy.ensureAwake()` |
217
-
218
- When the embed reaches `phase: 'live'` the overlay fades out.
219
-
220
- ### 3.5 Top bar
221
-
222
- Layout: `[icon] [app name] ........ [robot status] [end-session] [oauth menu]`
223
-
224
- - Left: app icon + app name from `mountHost()`.
225
- - Right: when `isLive`, an "End session" button; when also `isAwake`,
226
- a motors-awake dot; always (when `userName` known), the OAuth
227
- menu with @username and "Log out".
228
-
229
- The top bar stays rendered through every phase of Mode A. It does
230
- **not** render at all in Mode B.
231
-
232
- ### 3.6 End session
233
-
234
- Triggered by:
235
- - The user clicks "End session", OR
236
- - The embed posts `embed:request-leave`, OR
237
- - The window is about to unload (`pagehide`).
238
-
239
- Flow:
240
- 1. Host → phase `stopping`, `AppFrame` stays mounted with
241
- `LeavingView` overlay.
242
- 2. Host posts `host:leaving`.
243
- 3. Embed runs `onLeave` callbacks → `reachy.stopSession()` → posts
244
- `embed:custom { channel: "leave-ack" }`.
245
- 4. Host receives ack (or hits `timeoutMs`) → `unselectRobot()` →
246
- phase `picking`. Iframe unmounts.
247
-
248
- ---
249
-
250
- ## 4. Mode B: Mobile handoff flow
251
-
252
- The mobile app opens the Space in a WebView with a pre-built URL
253
- containing creds in the hash. The dispatcher loads `./embed`
254
- directly. **No host shell is mounted.**
255
-
256
- The user sees:
257
- - **No** sign-in view (mobile already authenticated).
258
- - **No** robot picker (mobile already picked).
259
- - **No** welcome-back animation.
260
- - **No** host top bar - if the app wants one, it draws it itself.
261
-
262
- The embed reads creds from the hash synchronously, wipes the hash,
263
- and runs the same `connect → startSession → ensureAwake` sequence.
264
-
265
- There is **no end-session button** in Mode B. Closing the WebView
266
- triggers `pagehide`, which fires `onLeave` callbacks and
267
- `reachy.stopSession()` automatically.
268
-
269
- ### Bridge in Mode B
270
-
271
- `window.parent === window`, so the postMessage bridge is a noop.
272
- The embed never expects a `host:init` from a parent; creds come
273
- from the hash. An optional `host:init` wait is best-effort with a
274
- short timeout so the flow never blocks.
275
-
276
- ---
277
-
278
- ## 5. App-author contract
279
-
280
- When porting a Reachy Mini app to this stack, the app author owns
281
- exactly four files:
282
-
283
- | File | Purpose |
284
- |-----------------------------|--------------------------------------------------------|
285
- | `index.html` | Theme bootstrap, SDK CDN tag, `<script src=dispatch>` |
286
- | `src/dispatch.ts` | Two-branch boot (host shell vs embed) |
287
- | `src/embed.{ts,tsx}` | The app itself; calls `connectToHost()` once |
288
- | `Dockerfile` | HF Spaces packaging (nginx + entrypoint var injection) |
289
-
290
- Nothing else is mandatory. React, Svelte, Vue, vanilla TS, plain
291
- HTML - all fine. See §1 "Tech freedom" for the rationale.
292
-
293
- ### Reference apps (canonical clone targets)
294
-
295
- Three apps in this monorepo are kept in sync with the current
296
- spec and serve as canonical starting points:
297
-
298
- - **`reachy_mini_emotions/`** - React 19 + MUI 7. Best starting
299
- point for a UI-rich app. Demonstrates `robot.set_full_target()`
300
- streaming, `?config=` deep-linking, and ThemeProvider wired to
301
- `host:theme-changed`.
302
- - **`reachy_mini_telepresence/`** - React 19 + MUI 7. Best
303
- starting point if your app needs camera / media streams in
304
- addition to motion control.
305
- - **`reachy_mini_minimal_conversation/`** - Vanilla TS, no
306
- framework. Best starting point if you want the smallest
307
- possible runtime overhead, or if your app's main UI is just an
308
- audio control surface.
309
-
310
- Any divergence between these apps and this SPEC is a bug - either
311
- in the apps or in the SPEC. Fix it in the same PR that ships the
312
- divergence.
313
-
314
- ---
315
-
316
- ## 6. Protocol v1
317
-
318
- Full type definitions in [`src/lib/protocol.ts`](./src/lib/protocol.ts).
319
- Envelopes are JSON, carry `source: 'reachy-mini'` and `version: 1`.
320
- Both sides validate `event.origin` against `window.location.origin`
321
- before trusting the payload.
322
-
323
- ### 6.1 Messages
324
-
325
- | Direction | Type | Purpose |
326
- |-----------------|-------------------------------|-----------------------------------------------|
327
- | embed → host | `embed:ready` | "I'm alive, send creds" |
328
- | host → embed | `host:init` | Theme, signaling URL, hfToken, robotPeerId, config |
329
- | embed → host | `embed:app-state` | Lifecycle phase + connecting sub-step |
330
- | host → embed | `host:theme-changed` | Theme switched (no reload) |
331
- | host → embed | `host:config-changed` | Config updated (no reload, mobile-driven) |
332
- | host → embed | `host:leaving` | Tear-down request with `timeoutMs` |
333
- | embed → host | `embed:request-leave` | App requests end-of-session |
334
- | embed → host | `embed:error` | Error report (`{ message, fatal, detail? }`) |
335
-
336
- Intentionally **not** in the v1 protocol:
337
-
338
- - `embed:request-config-update` (apps don't push config upstream).
339
- - `host:custom` / `embed:custom` (no free-form channel; if a real
340
- need emerges, it lands as a typed message via the major bump).
341
- - Any heartbeat / ping-pong messages (§11 backlog).
342
-
343
- ### 6.2 Versioning policy
344
-
345
- - `version: 1` is the only version today.
346
- - **Additive changes** (new optional field, new message type)
347
- ship without a version bump.
348
- - **Breaking changes** (removed field, changed semantics, removed
349
- message type) bump to `version: 2`. The host MAY support both
350
- versions for one release cycle, then drop v1.
351
- - On unknown version, the receiver logs a warning and ignores the
352
- message. No negotiation handshake.
353
-
354
- ### 6.3 Idempotency
355
-
356
- - `host:leaving` may arrive twice; the embed runs `onLeave`
357
- callbacks **once** (gated by a `pendingLeaveTokenRef`) and acks
358
- every time.
359
- - `host:init` may arrive twice (rare: bridge re-arm); the embed
360
- treats the latest as authoritative and re-applies theme / config.
361
- - The bridge is same-tab `postMessage` - order is guaranteed by
362
- the browser; duplicates only happen on explicit re-sends.
363
-
364
- ---
365
-
366
- ## 7. State machines
367
-
368
- ### 7.1 Host phase (Mode A only)
369
-
370
- ```mermaid
371
- stateDiagram-v2
372
- [*] --> booting
373
- booting --> sdk_missing: SDK load timeout
374
- booting --> signed_out: authenticate() false
375
- booting --> authenticated: authenticate() true
376
- booting --> error: ctor / clientId error
377
-
378
- signed_out --> booting: user clicks Sign in
379
- authenticated --> connecting: auto
380
- connecting --> connected: SSE welcome
381
- connecting --> error: HTTP / network fail
382
- connected --> picking
383
- picking --> handing_off: selectRobot()
384
- handing_off --> live: embed reports phase=live
385
- handing_off --> error: embed:error { fatal: true }
386
-
387
- live --> stopping: endSession() OR embed:request-leave
388
- stopping --> picking: leave-ack OR timeout
389
-
390
- error --> picking: retry() if SDK authed
391
- error --> booting: retry() if no SDK
392
- error --> signed_out: retry() if auth expired
393
- ```
394
-
395
- ### 7.2 Handoff sequence (host → embed)
396
-
397
- Showcases §8.1 (single SDK per tab).
398
-
399
- ```mermaid
400
- sequenceDiagram
401
- participant U as User
402
- participant H as Host
403
- participant HS as Host SDK
404
- participant C as Central
405
- participant E as Embed (iframe)
406
- participant ES as Embed SDK
407
-
408
- U->>H: click robot card
409
- H->>H: phase = handing-off, mount iframe
410
- E->>E: parse #creds, replaceState wipe
411
- E->>H: embed:ready
412
- H->>HS: disconnect() [§8.1]
413
- H->>E: host:init
414
- E->>ES: connect() → startSession() → ensureAwake()
415
- ES->>C: claim robot
416
- C-->>ES: session live
417
- E->>H: embed:app-state { phase: 'live' }
418
- H->>H: hide ConnectingView overlay
419
- ```
420
-
421
- ---
422
-
423
- ## 8. Engineering invariants
424
-
425
- Three hard invariants. A failure here is a regression - it must
426
- produce a defined observable behaviour and must be covered by a
427
- test (see §11).
428
-
429
- ### 8.1 Single live SDK per tab
430
-
431
- **Contract**: at any instant, exactly one SDK instance per tab is
432
- registered at the central.
433
-
434
- **Mechanism**:
435
- - Host mounts the SDK in `booting` and uses it for the picker.
436
- - As soon as the embed posts `embed:ready`, the host calls
437
- `disconnect()` on its SDK and zeroes its references.
438
- - On `leave-ack` (or timeout), the host calls `connect()` again to
439
- refresh the fleet and lands back on `picking`.
440
-
441
- **Why**: removes the entire class of "Robot is busy" false
442
- positives where the central thinks the shell still owns the robot.
443
- Makes the central's debug output unambiguous.
444
-
445
- **Defence in depth**: the host's SDK registers as
446
- `<appName> (shell)` at the central; the embed keeps the clean
447
- `appName`. This safety net protects the narrow window where both
448
- SDKs overlap (between `embed:ready` and `disconnect()`).
449
-
450
- ### 8.2 Hash-only creds + immediate wipe
451
-
452
- **Contract**: HF tokens never appear in URL search, never in
453
- referer, never in HF Spaces access logs.
454
-
455
- **Mechanism**:
456
- - Creds are serialised as base64 JSON in the URL hash fragment
457
- (`#creds=...`).
458
- - The embed wipes the hash with `history.replaceState` on the
459
- first synchronous tick of `connectToHost()`, before any `await`.
460
- - On `host:leaving`, the embed clears `sessionStorage.hf_*` keys
461
- before sending the `leave-ack`.
462
-
463
- **Token TTL**: `hf_token_expires` is set to **15 min** at seed
464
- time. The SDK refreshes on demand.
465
-
466
- ### 8.3 Bundle and SDK pinning
467
-
468
- **Contract**: a fix in the host reaches every Space within one
469
- cache cycle, with no per-app rebuild. A fix in the Reachy Mini SDK
470
- can be rolled out by the SDK team, not the app teams.
471
-
472
- **Pinning rules**:
473
- - App bundles (`index-<hash>.js`): hashed by Vite, cache-busted
474
- on deploy.
475
- - Reachy Mini SDK in `index.html`: pinned to a **git tag or commit
476
- SHA**, never to a branch. jsdelivr caches branches for ~12 h.
477
- - `@pollen-robotics/reachy-mini-sdk/host` from CDN: pinned to a **major version**
478
- (`@pollen-robotics/reachy-mini-sdk@1`) on jsdelivr/unpkg.
479
-
480
- On detected mismatch (e.g. unknown protocol version, structurally
481
- invalid `host:init`), the host's ErrorView's primary button calls
482
- `window.location.reload(true)` to bypass any intermediary cache.
483
-
484
- ### 8.4 React Strict Mode safety
485
-
486
- **Contract**: every effect in the host package survives a double
487
- mount in `<React.StrictMode>` without doubling network I/O, SDK
488
- instances, or postMessage traffic.
489
-
490
- **Why this matters**: React 18+ in dev intentionally mounts every
491
- component, runs every effect, runs every cleanup, then mounts and
492
- runs effects again. Code that "looked fine" in prod will fire two
493
- parallel `connect()` calls, leak two SSE subscribers, post
494
- `embed:ready` twice, etc. In Mode A this surfaces as ghost
495
- sessions at the central; in Mode B it surfaces as two competing
496
- WebRTC peer connections.
497
-
498
- **Mechanisms**:
499
-
500
- - **Boot guard refs**: `useReachyHost()` uses a `bootStartedRef`
501
- set on first mount; the second StrictMode invocation early-
502
- returns instead of re-instantiating the SDK.
503
- - **Subscription cleanup**: every `useEffect` returns its tear-
504
- down. `robotsChanged`, `phaseChanged`, `welcome` are all
505
- unsubscribed in the cleanup.
506
- - **Idempotent SDK calls**: `connect()` is a no-op when the SDK
507
- is already `connected` / `streaming`; the host relies on this
508
- rather than gating with a flag.
509
- - **`connectToHost()` is one-shot**: a module-level
510
- `bootPromiseRef` returns the same promise for a second call,
511
- rather than re-running the handshake.
512
-
513
- **Checks**: the rebuild MUST verify this by mounting `<ReachyHost>`
514
- inside `<StrictMode>` in dev and observing in DevTools:
515
-
516
- 1. Exactly one SDK instance ever exists at the central.
517
- 2. Exactly one `embed:ready` is posted per iframe mount.
518
- 3. No "subscription leak" warnings from React's effect runner.
519
-
520
- If any of these fails, the implementation has a latent ordering
521
- bug; fix before publishing.
522
-
523
- ---
524
-
525
- ## 9. Non-goals
526
-
527
- To stay simple and auditable, this system explicitly does NOT do:
528
-
529
- - **App discovery / listing / catalog**. The host shell renders
530
- exactly **one** app: the one it ships with. Listing the available
531
- Reachy Mini apps, searching them, categorising them - all of
532
- that lives in the **Reachy Mini mobile app** and is fed by the
533
- **website's `/api/js-apps` endpoint** (filtered on the
534
- `reachy_mini_js_app` tag). The host has no notion of "other apps".
535
- - **"Official app" badging in the host shell**. Officialness is
536
- derived from the Space ID prefix (`pollen-robotics/*`, see §1)
537
- and surfaces only in the mobile catalog. The host renders any
538
- app the same way.
539
- - **Multi-robot per tab**. One robot at a time per Space session.
540
- - **Hot reload of the app code without reloading the iframe**.
541
- Code updates require unmounting and remounting.
542
- - **App ↔ app communication**. Apps are sandboxed by design.
543
- - **Offline mode**. The central is required for every session.
544
- - **Automatic WebRTC retry**. On a session drop, the user goes
545
- back to the picker manually.
546
- - **Queue or persistence of postMessage events** if the bridge is
547
- down. The bridge is best-effort, both sides re-converge on the
548
- next message.
549
- - **Server-side rendering**. The host is a CSR shell, deliberately.
550
- - **Cross-origin iframe**. The embed is same-origin with the host
551
- (both served by the Space's nginx); we rely on this for the
552
- origin check.
553
- - **Vanilla / framework-agnostic host shell**. The host is
554
- intentionally React + MUI (§1). Apps in any framework load this
555
- shell only for the pre-session UI; once they go live in the
556
- iframe, the shell's React tree is idle. Re-evaluating this is
557
- in §11 backlog if real usage data ever justifies it.
558
- - **Imposing a framework on app authors**. The reverse of the
559
- point above: apps are completely free of framework constraints
560
- inside the iframe (see §1 Tech freedom).
561
-
562
- ---
563
-
564
- ## 10. Threat model
565
-
566
- The host runs in a HF Spaces container; the embed runs in a
567
- same-origin iframe within the same Space. The threat surface is
568
- narrow but not zero.
569
-
570
- | Asset | Threat | Mitigation |
571
- |--------------------------|------------------------------------------|------------------------------------------------|
572
- | HF bearer token | Leak via URL log / referer | Hash-only + immediate wipe (§8.2), 15 min TTL |
573
- | HF bearer token | Leak via sessionStorage to other origin | Same-origin embed, no cross-origin postMessage |
574
- | `config` payload | Attacker controls URL → malformed JSON | App MUST validate at the boundary (typed cast is not enough) |
575
- | postMessage channel | Random extension posts a forged message | Receivers check `source === 'reachy-mini'` AND `event.origin === window.location.origin` |
576
- | Central session | Tab crashes, robot stays claimed | `pagehide` triggers `stopSession()`; central also enforces idle timeout |
577
-
578
- **Out of scope** for this iteration:
579
- - Defence against a malicious app that the user explicitly loaded.
580
- We trust apps published under the HF Reachy Mini namespace.
581
- - Defence against a compromised central. The signaling URL is
582
- configurable per-Space; the central is the trust root.
583
-
584
- ---
585
-
586
- ## 11. Backlog
587
-
588
- Items below are tracked but **not** in §8 yet. Promote to §8 only
589
- when implemented and the cost of regressing them is justified.
590
-
591
- - **CDN-only host**: the historical workaround of vendoring
592
- `dist/` into `vendor/reachy-mini-host/` to placate HF Spaces'
593
- Docker build (which refused `file:../` deps) is no longer
594
- needed now that `@pollen-robotics/reachy-mini-sdk/host` is
595
- published to npm. The dispatcher imports from the jsdelivr
596
- bundle, so a host fix reaches every Space without per-app
597
- redeploys.
598
- - **Heartbeat / liveness**: detect a frozen embed (JS infinite
599
- loop in a same-tab iframe). Trigger if we observe a freeze in
600
- the wild. Until then, the user can hit "End session" manually.
601
- - **Structured error codes**: today `embed:error` is `{ message,
602
- fatal, detail? }`. If error reporting becomes a real ops need,
603
- introduce a `code: 'busy' | 'auth' | 'network' | 'startup' |
604
- 'unknown'` enum.
605
- - **Short-lived scoped tokens**: replace the raw HF token in
606
- `CredsBundle.hfToken` with a session-scoped token issued by a
607
- small backend.
608
- - **Reconnect after session drop**: SDK could expose
609
- `resumeSession()` so we re-run the WebRTC handshake without
610
- going through the picker again.
611
- - **Mobile parity for ConnectingView**: export a tiny
612
- `<EmbedConnectingOverlay>` from `@pollen-robotics/reachy-mini-sdk/host/embed` so
613
- Mode B apps reuse the 3-step visual without pulling React.
614
- - **Observability hook**: opt-in callback for app authors / HF ops
615
- to collect a redacted trace of every session.
616
- - **Test suite**: unit tests on `useHostBridge` and
617
- `connectToHost`, e2e Playwright run against a mock central. The
618
- invariants in §8 are the prime testing target.