@glassly/miniapp 0.3.0-dev.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 (312) hide show
  1. package/README.md +275 -0
  2. package/dist/background/index.d.ts +64 -0
  3. package/dist/background/index.d.ts.map +1 -0
  4. package/dist/background/index.js +26 -0
  5. package/dist/background/index.js.map +1 -0
  6. package/dist/background/register.d.ts +39 -0
  7. package/dist/background/register.d.ts.map +1 -0
  8. package/dist/background/register.js +72 -0
  9. package/dist/background/register.js.map +1 -0
  10. package/dist/dev-reload.d.ts +21 -0
  11. package/dist/dev-reload.d.ts.map +1 -0
  12. package/dist/dev-reload.js +60 -0
  13. package/dist/dev-reload.js.map +1 -0
  14. package/dist/envelope.d.ts +23 -0
  15. package/dist/envelope.d.ts.map +1 -0
  16. package/dist/envelope.js +47 -0
  17. package/dist/envelope.js.map +1 -0
  18. package/dist/globals.d.ts +54 -0
  19. package/dist/globals.d.ts.map +1 -0
  20. package/dist/globals.js +12 -0
  21. package/dist/globals.js.map +1 -0
  22. package/dist/hardware.d.ts +38 -0
  23. package/dist/hardware.d.ts.map +1 -0
  24. package/dist/hardware.js +31 -0
  25. package/dist/hardware.js.map +1 -0
  26. package/dist/index.d.ts +65 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +34 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/modules/actions.d.ts +81 -0
  31. package/dist/modules/actions.d.ts.map +1 -0
  32. package/dist/modules/actions.js +137 -0
  33. package/dist/modules/actions.js.map +1 -0
  34. package/dist/modules/ai.d.ts +211 -0
  35. package/dist/modules/ai.d.ts.map +1 -0
  36. package/dist/modules/ai.js +117 -0
  37. package/dist/modules/ai.js.map +1 -0
  38. package/dist/modules/auth.d.ts +25 -0
  39. package/dist/modules/auth.d.ts.map +1 -0
  40. package/dist/modules/auth.js +52 -0
  41. package/dist/modules/auth.js.map +1 -0
  42. package/dist/modules/base64.d.ts +16 -0
  43. package/dist/modules/base64.d.ts.map +1 -0
  44. package/dist/modules/base64.js +92 -0
  45. package/dist/modules/base64.js.map +1 -0
  46. package/dist/modules/blob.d.ts +161 -0
  47. package/dist/modules/blob.d.ts.map +1 -0
  48. package/dist/modules/blob.js +277 -0
  49. package/dist/modules/blob.js.map +1 -0
  50. package/dist/modules/camera.d.ts +183 -0
  51. package/dist/modules/camera.d.ts.map +1 -0
  52. package/dist/modules/camera.js +119 -0
  53. package/dist/modules/camera.js.map +1 -0
  54. package/dist/modules/cloud.d.ts +34 -0
  55. package/dist/modules/cloud.d.ts.map +1 -0
  56. package/dist/modules/cloud.js +70 -0
  57. package/dist/modules/cloud.js.map +1 -0
  58. package/dist/modules/dashboard.d.ts +24 -0
  59. package/dist/modules/dashboard.d.ts.map +1 -0
  60. package/dist/modules/dashboard.js +39 -0
  61. package/dist/modules/dashboard.js.map +1 -0
  62. package/dist/modules/display.d.ts +167 -0
  63. package/dist/modules/display.d.ts.map +1 -0
  64. package/dist/modules/display.js +83 -0
  65. package/dist/modules/display.js.map +1 -0
  66. package/dist/modules/events.d.ts +178 -0
  67. package/dist/modules/events.d.ts.map +1 -0
  68. package/dist/modules/events.js +168 -0
  69. package/dist/modules/events.js.map +1 -0
  70. package/dist/modules/glasses.d.ts +36 -0
  71. package/dist/modules/glasses.d.ts.map +1 -0
  72. package/dist/modules/glasses.js +52 -0
  73. package/dist/modules/glasses.js.map +1 -0
  74. package/dist/modules/heading.d.ts +16 -0
  75. package/dist/modules/heading.d.ts.map +1 -0
  76. package/dist/modules/heading.js +20 -0
  77. package/dist/modules/heading.js.map +1 -0
  78. package/dist/modules/imu.d.ts +31 -0
  79. package/dist/modules/imu.d.ts.map +1 -0
  80. package/dist/modules/imu.js +40 -0
  81. package/dist/modules/imu.js.map +1 -0
  82. package/dist/modules/input.d.ts +34 -0
  83. package/dist/modules/input.d.ts.map +1 -0
  84. package/dist/modules/input.js +50 -0
  85. package/dist/modules/input.js.map +1 -0
  86. package/dist/modules/keys.d.ts +30 -0
  87. package/dist/modules/keys.d.ts.map +1 -0
  88. package/dist/modules/keys.js +37 -0
  89. package/dist/modules/keys.js.map +1 -0
  90. package/dist/modules/languages.d.ts +42 -0
  91. package/dist/modules/languages.d.ts.map +1 -0
  92. package/dist/modules/languages.js +57 -0
  93. package/dist/modules/languages.js.map +1 -0
  94. package/dist/modules/led.d.ts +35 -0
  95. package/dist/modules/led.d.ts.map +1 -0
  96. package/dist/modules/led.js +39 -0
  97. package/dist/modules/led.js.map +1 -0
  98. package/dist/modules/location.d.ts +27 -0
  99. package/dist/modules/location.d.ts.map +1 -0
  100. package/dist/modules/location.js +35 -0
  101. package/dist/modules/location.js.map +1 -0
  102. package/dist/modules/memory.d.ts +126 -0
  103. package/dist/modules/memory.d.ts.map +1 -0
  104. package/dist/modules/memory.js +141 -0
  105. package/dist/modules/memory.js.map +1 -0
  106. package/dist/modules/mic.d.ts +61 -0
  107. package/dist/modules/mic.d.ts.map +1 -0
  108. package/dist/modules/mic.js +94 -0
  109. package/dist/modules/mic.js.map +1 -0
  110. package/dist/modules/miniapps.d.ts +82 -0
  111. package/dist/modules/miniapps.d.ts.map +1 -0
  112. package/dist/modules/miniapps.js +58 -0
  113. package/dist/modules/miniapps.js.map +1 -0
  114. package/dist/modules/navigation.d.ts +332 -0
  115. package/dist/modules/navigation.d.ts.map +1 -0
  116. package/dist/modules/navigation.js +387 -0
  117. package/dist/modules/navigation.js.map +1 -0
  118. package/dist/modules/permissions.d.ts +58 -0
  119. package/dist/modules/permissions.d.ts.map +1 -0
  120. package/dist/modules/permissions.js +66 -0
  121. package/dist/modules/permissions.js.map +1 -0
  122. package/dist/modules/phone.d.ts +95 -0
  123. package/dist/modules/phone.d.ts.map +1 -0
  124. package/dist/modules/phone.js +110 -0
  125. package/dist/modules/phone.js.map +1 -0
  126. package/dist/modules/pivots/engine.d.ts +169 -0
  127. package/dist/modules/pivots/engine.d.ts.map +1 -0
  128. package/dist/modules/pivots/engine.js +778 -0
  129. package/dist/modules/pivots/engine.js.map +1 -0
  130. package/dist/modules/pivots/geometry.d.ts +84 -0
  131. package/dist/modules/pivots/geometry.d.ts.map +1 -0
  132. package/dist/modules/pivots/geometry.js +371 -0
  133. package/dist/modules/pivots/geometry.js.map +1 -0
  134. package/dist/modules/pivots/instructions.d.ts +139 -0
  135. package/dist/modules/pivots/instructions.d.ts.map +1 -0
  136. package/dist/modules/pivots/instructions.js +281 -0
  137. package/dist/modules/pivots/instructions.js.map +1 -0
  138. package/dist/modules/speaker.d.ts +175 -0
  139. package/dist/modules/speaker.d.ts.map +1 -0
  140. package/dist/modules/speaker.js +249 -0
  141. package/dist/modules/speaker.js.map +1 -0
  142. package/dist/modules/speakers.d.ts +29 -0
  143. package/dist/modules/speakers.d.ts.map +1 -0
  144. package/dist/modules/speakers.js +38 -0
  145. package/dist/modules/speakers.js.map +1 -0
  146. package/dist/modules/storage.d.ts +50 -0
  147. package/dist/modules/storage.d.ts.map +1 -0
  148. package/dist/modules/storage.js +99 -0
  149. package/dist/modules/storage.js.map +1 -0
  150. package/dist/modules/stream.d.ts +133 -0
  151. package/dist/modules/stream.d.ts.map +1 -0
  152. package/dist/modules/stream.js +53 -0
  153. package/dist/modules/stream.js.map +1 -0
  154. package/dist/modules/system.d.ts +71 -0
  155. package/dist/modules/system.d.ts.map +1 -0
  156. package/dist/modules/system.js +65 -0
  157. package/dist/modules/system.js.map +1 -0
  158. package/dist/modules/transcription.d.ts +99 -0
  159. package/dist/modules/transcription.d.ts.map +1 -0
  160. package/dist/modules/transcription.js +126 -0
  161. package/dist/modules/transcription.js.map +1 -0
  162. package/dist/modules/translation.d.ts +65 -0
  163. package/dist/modules/translation.d.ts.map +1 -0
  164. package/dist/modules/translation.js +121 -0
  165. package/dist/modules/translation.js.map +1 -0
  166. package/dist/modules/ui.d.ts +186 -0
  167. package/dist/modules/ui.d.ts.map +1 -0
  168. package/dist/modules/ui.js +289 -0
  169. package/dist/modules/ui.js.map +1 -0
  170. package/dist/protocol.d.ts +384 -0
  171. package/dist/protocol.d.ts.map +1 -0
  172. package/dist/protocol.js +413 -0
  173. package/dist/protocol.js.map +1 -0
  174. package/dist/react/MiniappHeader.d.ts +44 -0
  175. package/dist/react/MiniappHeader.d.ts.map +1 -0
  176. package/dist/react/MiniappHeader.js +32 -0
  177. package/dist/react/MiniappHeader.js.map +1 -0
  178. package/dist/react/MiniappProvider.d.ts +33 -0
  179. package/dist/react/MiniappProvider.d.ts.map +1 -0
  180. package/dist/react/MiniappProvider.js +15 -0
  181. package/dist/react/MiniappProvider.js.map +1 -0
  182. package/dist/react/capsuleHeaderLayout.d.ts +3 -0
  183. package/dist/react/capsuleHeaderLayout.d.ts.map +1 -0
  184. package/dist/react/capsuleHeaderLayout.js +5 -0
  185. package/dist/react/capsuleHeaderLayout.js.map +1 -0
  186. package/dist/react/index.d.ts +13 -0
  187. package/dist/react/index.d.ts.map +1 -0
  188. package/dist/react/index.js +9 -0
  189. package/dist/react/index.js.map +1 -0
  190. package/dist/react/useCapabilities.d.ts +26 -0
  191. package/dist/react/useCapabilities.d.ts.map +1 -0
  192. package/dist/react/useCapabilities.js +27 -0
  193. package/dist/react/useCapabilities.js.map +1 -0
  194. package/dist/react/useCapsuleHeaderStyle.d.ts +27 -0
  195. package/dist/react/useCapsuleHeaderStyle.d.ts.map +1 -0
  196. package/dist/react/useCapsuleHeaderStyle.js +39 -0
  197. package/dist/react/useCapsuleHeaderStyle.js.map +1 -0
  198. package/dist/react/useColorScheme.d.ts +18 -0
  199. package/dist/react/useColorScheme.d.ts.map +1 -0
  200. package/dist/react/useColorScheme.js +24 -0
  201. package/dist/react/useColorScheme.js.map +1 -0
  202. package/dist/react/useConnected.d.ts +17 -0
  203. package/dist/react/useConnected.d.ts.map +1 -0
  204. package/dist/react/useConnected.js +39 -0
  205. package/dist/react/useConnected.js.map +1 -0
  206. package/dist/react/useRpc.d.ts +29 -0
  207. package/dist/react/useRpc.d.ts.map +1 -0
  208. package/dist/react/useRpc.js +82 -0
  209. package/dist/react/useRpc.js.map +1 -0
  210. package/dist/react/useSafeArea.d.ts +21 -0
  211. package/dist/react/useSafeArea.d.ts.map +1 -0
  212. package/dist/react/useSafeArea.js +22 -0
  213. package/dist/react/useSafeArea.js.map +1 -0
  214. package/dist/session.d.ts +355 -0
  215. package/dist/session.d.ts.map +1 -0
  216. package/dist/session.js +688 -0
  217. package/dist/session.js.map +1 -0
  218. package/dist/sub-path-types.test-d.d.ts +40 -0
  219. package/dist/sub-path-types.test-d.d.ts.map +1 -0
  220. package/dist/sub-path-types.test-d.js +37 -0
  221. package/dist/sub-path-types.test-d.js.map +1 -0
  222. package/dist/transport/auto.d.ts +35 -0
  223. package/dist/transport/auto.d.ts.map +1 -0
  224. package/dist/transport/auto.js +124 -0
  225. package/dist/transport/auto.js.map +1 -0
  226. package/dist/transport/dispatch.d.ts +43 -0
  227. package/dist/transport/dispatch.d.ts.map +1 -0
  228. package/dist/transport/dispatch.js +91 -0
  229. package/dist/transport/dispatch.js.map +1 -0
  230. package/dist/transport/local-socket.d.ts +28 -0
  231. package/dist/transport/local-socket.d.ts.map +1 -0
  232. package/dist/transport/local-socket.js +80 -0
  233. package/dist/transport/local-socket.js.map +1 -0
  234. package/dist/transport/mock.d.ts +54 -0
  235. package/dist/transport/mock.d.ts.map +1 -0
  236. package/dist/transport/mock.js +210 -0
  237. package/dist/transport/mock.js.map +1 -0
  238. package/dist/transport/postmessage.d.ts +36 -0
  239. package/dist/transport/postmessage.d.ts.map +1 -0
  240. package/dist/transport/postmessage.js +78 -0
  241. package/dist/transport/postmessage.js.map +1 -0
  242. package/dist/transport/types.d.ts +25 -0
  243. package/dist/transport/types.d.ts.map +1 -0
  244. package/dist/transport/types.js +11 -0
  245. package/dist/transport/types.js.map +1 -0
  246. package/dist/ui/index.d.ts +107 -0
  247. package/dist/ui/index.d.ts.map +1 -0
  248. package/dist/ui/index.js +32 -0
  249. package/dist/ui/index.js.map +1 -0
  250. package/package.json +84 -0
  251. package/src/background/index.ts +177 -0
  252. package/src/background/register.ts +89 -0
  253. package/src/dev-reload.ts +59 -0
  254. package/src/envelope.ts +54 -0
  255. package/src/globals.ts +62 -0
  256. package/src/hardware.ts +40 -0
  257. package/src/index.ts +216 -0
  258. package/src/modules/actions.ts +189 -0
  259. package/src/modules/ai.ts +281 -0
  260. package/src/modules/auth.ts +68 -0
  261. package/src/modules/base64.ts +87 -0
  262. package/src/modules/blob.ts +341 -0
  263. package/src/modules/camera.ts +258 -0
  264. package/src/modules/cloud.ts +94 -0
  265. package/src/modules/dashboard.ts +51 -0
  266. package/src/modules/display.ts +207 -0
  267. package/src/modules/events.ts +318 -0
  268. package/src/modules/glasses.ts +58 -0
  269. package/src/modules/heading.ts +23 -0
  270. package/src/modules/imu.ts +44 -0
  271. package/src/modules/input.ts +66 -0
  272. package/src/modules/keys.ts +46 -0
  273. package/src/modules/languages.ts +72 -0
  274. package/src/modules/led.ts +61 -0
  275. package/src/modules/location.ts +39 -0
  276. package/src/modules/memory.ts +224 -0
  277. package/src/modules/mic.ts +103 -0
  278. package/src/modules/miniapps.ts +103 -0
  279. package/src/modules/navigation.ts +674 -0
  280. package/src/modules/permissions.ts +83 -0
  281. package/src/modules/phone.ts +155 -0
  282. package/src/modules/pivots/engine.ts +863 -0
  283. package/src/modules/pivots/geometry.ts +409 -0
  284. package/src/modules/pivots/instructions.ts +341 -0
  285. package/src/modules/speaker.ts +347 -0
  286. package/src/modules/speakers.ts +40 -0
  287. package/src/modules/storage.ts +109 -0
  288. package/src/modules/stream.ts +168 -0
  289. package/src/modules/system.ts +113 -0
  290. package/src/modules/transcription.ts +180 -0
  291. package/src/modules/translation.ts +156 -0
  292. package/src/modules/ui.ts +481 -0
  293. package/src/protocol.ts +475 -0
  294. package/src/react/MiniappHeader.tsx +114 -0
  295. package/src/react/MiniappProvider.tsx +50 -0
  296. package/src/react/capsuleHeaderLayout.ts +11 -0
  297. package/src/react/index.ts +12 -0
  298. package/src/react/useCapabilities.ts +29 -0
  299. package/src/react/useCapsuleHeaderStyle.ts +55 -0
  300. package/src/react/useColorScheme.ts +26 -0
  301. package/src/react/useConnected.ts +47 -0
  302. package/src/react/useRpc.ts +103 -0
  303. package/src/react/useSafeArea.ts +36 -0
  304. package/src/session.ts +969 -0
  305. package/src/sub-path-types.test-d.ts +60 -0
  306. package/src/transport/auto.ts +143 -0
  307. package/src/transport/dispatch.ts +106 -0
  308. package/src/transport/local-socket.ts +91 -0
  309. package/src/transport/mock.ts +255 -0
  310. package/src/transport/postmessage.ts +95 -0
  311. package/src/transport/types.ts +26 -0
  312. package/src/ui/index.ts +129 -0
package/src/session.ts ADDED
@@ -0,0 +1,969 @@
1
+ /**
2
+ * @fileoverview MiniappSession — central session object for a local miniapp.
3
+ *
4
+ * Owns the transport, the request/response correlation map, the readiness queue,
5
+ * the PONG auto-reply, the visibility state, and all per-module instances.
6
+ *
7
+ * Lifecycle:
8
+ * const session = new MiniappSession()
9
+ * await session.connect() // sends CONNECT, resolves on CONNECT_ACK
10
+ * session.display.render([...])
11
+ * ...
12
+ * session.disconnect()
13
+ */
14
+
15
+ import {EventEmitter} from "eventemitter3"
16
+
17
+ import {makeRequestId, MiniappEnvelope, parseEnvelope, serializeEnvelope} from "./envelope"
18
+ import {getGlasslyGlobals, MiniappColorScheme} from "./globals"
19
+ import {MiniappErrorCode, MiniappRequestType, MiniappResponseType} from "./protocol"
20
+ import {createTransport, CreateTransportOptions} from "./transport/auto"
21
+ import {Transport} from "./transport/types"
22
+ import {CameraModule} from "./modules/camera"
23
+ import {AuthModule} from "./modules/auth"
24
+ import {CloudModule} from "./modules/cloud"
25
+ import {DashboardAPI} from "./modules/dashboard"
26
+ import {DisplayManager} from "./modules/display"
27
+ import {EventManager, type TranscriptionEventRoute, type UnsubscribeFn} from "./modules/events"
28
+ import {GlassesModule} from "./modules/glasses"
29
+ import {HeadingModule} from "./modules/heading"
30
+ import {AiModule} from "./modules/ai"
31
+ import {ImuModule} from "./modules/imu"
32
+ import {InputModule} from "./modules/input"
33
+ import {KeysModule} from "./modules/keys"
34
+ import {LedModule} from "./modules/led"
35
+ import {LocationModule} from "./modules/location"
36
+ import {MemoryModule} from "./modules/memory"
37
+ import {SpeakersModule} from "./modules/speakers"
38
+ import {MicModule} from "./modules/mic"
39
+ import {NavigationModule} from "./modules/navigation"
40
+ import {PermissionsModule} from "./modules/permissions"
41
+ import {PhoneModule} from "./modules/phone"
42
+ import {TranscriptionModule} from "./modules/transcription"
43
+ import {TranslationModule} from "./modules/translation"
44
+ import {UIModuleImpl, type UIModule} from "./modules/ui"
45
+ import {SimpleStorage} from "./modules/storage"
46
+ import {SpeakerModule} from "./modules/speaker"
47
+ import {StreamModule} from "./modules/stream"
48
+ import {SystemModule} from "./modules/system"
49
+ import {MiniappsModule} from "./modules/miniapps"
50
+ import {ActionsModule} from "./modules/actions"
51
+ import {BlobModule} from "./modules/blob"
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Public types
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /**
58
+ * Typed display capabilities for the scene API. All limit fields are optional
59
+ * in the type because older hosts don't send them — treat absence as "unknown",
60
+ * not zero. Populated on the "ready" event (null in `start()`).
61
+ */
62
+ export interface DisplayCapabilities {
63
+ /** Public drawable canvas in px — raw coordinate space for `display.render()` boxes. */
64
+ width?: number
65
+ height?: number
66
+ /** False ⇒ the device can't position elements; scenes degrade to text walls host-side. */
67
+ canPosition?: boolean
68
+ /** Element budgets. Rects share the text pool on container-based devices. */
69
+ maxTextElements?: number
70
+ maxImageElements?: number
71
+ /** Per-image dimension cap (box-level), when the device has one. */
72
+ maxImagePx?: {width: number; height: number}
73
+ shapes?: string[]
74
+ intensityLevels?: number
75
+ partialUpdate?: boolean
76
+ /** Legacy capability fields (resolution, isColor, maxTextLines, …) ride along. */
77
+ [key: string]: unknown
78
+ }
79
+
80
+ /** Minimal snapshot of the currently-connected glasses. Phone-provided. */
81
+ export interface GlassesCapabilities {
82
+ /** Display block — null/absent on displayless devices (e.g. Mentra Live). */
83
+ display?: DisplayCapabilities | null
84
+ /** Device renders its own turn-by-turn nav UI in firmware (see display.sendNativeNav*). */
85
+ hasNativeNavigation?: boolean
86
+ [key: string]: unknown
87
+ }
88
+
89
+ export type MiniappVisibility = "foreground" | "background"
90
+
91
+ export interface MiniappSessionOptions extends CreateTransportOptions {
92
+ /** Override auto-detected packageName. Normally provided via window.Glassly. */
93
+ packageName?: string
94
+ /** Override the ready timeout. Default 10s. */
95
+ connectTimeoutMs?: number
96
+ }
97
+
98
+ export interface ConnectAckPayload {
99
+ type: MiniappResponseType.CONNECT_ACK
100
+ userId: string
101
+ packageName: string
102
+ capabilities: GlassesCapabilities | null
103
+ visibility?: MiniappVisibility
104
+ colorScheme?: MiniappColorScheme
105
+ /**
106
+ * Manifest-declared permission record. Mirrors cloud SDK v3's PermissionRecord:
107
+ * `{location, microphone, camera, notifications, calendar, apiKeys}` —
108
+ * booleans indicating whether the miniapp's manifest declared each. This is
109
+ * declaration-only; OS-grant state is not modeled.
110
+ */
111
+ permissions?: PermissionRecord
112
+ /** Miniapp-scoped backend auth. Never a Core or runtime token. */
113
+ auth?: MiniappAuthState
114
+ }
115
+
116
+ export interface MiniappAuthState {
117
+ glasslyUserId: string
118
+ oemId?: string
119
+ token: string
120
+ expiresAt: number
121
+ }
122
+
123
+ export interface AuthUpdatePayload {
124
+ type: MiniappResponseType.AUTH_UPDATE
125
+ auth?: MiniappAuthState
126
+ }
127
+
128
+ interface AuthRefreshResult {
129
+ auth?: MiniappAuthState
130
+ }
131
+
132
+ /**
133
+ * Manifest-declared permission record. v3-aligned: lowercase canonical keys.
134
+ * Booleans indicate whether the miniapp declared each in its manifest.json.
135
+ */
136
+ export type PermissionType =
137
+ | "location"
138
+ | "microphone"
139
+ | "camera"
140
+ | "notifications"
141
+ | "calendar"
142
+ | "apiKeys"
143
+ | "memory"
144
+ | "ai"
145
+ | "speakers"
146
+ export type PermissionRecord = Record<PermissionType, boolean>
147
+
148
+ const ALL_PERMISSION_TYPES: readonly PermissionType[] = [
149
+ "location",
150
+ "microphone",
151
+ "camera",
152
+ "notifications",
153
+ "calendar",
154
+ "apiKeys",
155
+ "memory",
156
+ "ai",
157
+ "speakers",
158
+ ] as const
159
+
160
+ export class NotConnectedError extends Error {
161
+ readonly code = MiniappErrorCode.NOT_CONNECTED
162
+ constructor(message = "MiniappSession is not connected") {
163
+ super(message)
164
+ this.name = "NotConnectedError"
165
+ }
166
+ }
167
+
168
+ export interface MiniappRequestError {
169
+ code: string
170
+ message: string
171
+ }
172
+
173
+ /**
174
+ * What a request promise actually rejects with: a real Error (so `${err}`,
175
+ * `err.message`, and stack traces behave) that still carries the host's
176
+ * `code` for callers matching on MiniappRequestError's shape. Before this
177
+ * class, requests rejected with the bare `{code, message}` wire object and
178
+ * miniapp logs printed "[object Object]".
179
+ */
180
+ export class MiniappRequestFailure extends Error implements MiniappRequestError {
181
+ constructor(
182
+ readonly code: string,
183
+ message: string,
184
+ ) {
185
+ super(message)
186
+ this.name = "MiniappRequestFailure"
187
+ }
188
+ }
189
+
190
+ // ---------------------------------------------------------------------------
191
+ // Implementation
192
+ // ---------------------------------------------------------------------------
193
+
194
+ interface PendingRequest {
195
+ requestId: string
196
+ resolve: (value: unknown) => void
197
+ reject: (error: MiniappRequestError) => void
198
+ /** Timeout handle; cleared when the response (or a transport failure) settles the request. */
199
+ timer?: ReturnType<typeof setTimeout>
200
+ }
201
+
202
+ const DEFAULT_CONNECT_TIMEOUT_MS = 10_000
203
+
204
+ // Hard ceiling on how long a single bridge request waits for the host's
205
+ // REQUEST_RESULT. Without it, a host that never responds (a hung cloud call, a
206
+ // GPS fix that never arrives, a native handler that stalls) leaves the request
207
+ // promise pending FOREVER — which is what stalled navigation at "Starting…"
208
+ // (the controller's `starting` guard never reset because start() never settled).
209
+ // 60s is generous: it covers a slow route computation while still guaranteeing
210
+ // the promise eventually rejects so callers can roll back / surface an error.
211
+ const DEFAULT_REQUEST_TIMEOUT_MS = 60_000
212
+
213
+ type SessionEmitterEvents = {
214
+ ready: () => void
215
+ error: (error: Error) => void
216
+ /**
217
+ * Last-chance hook before the transport closes. Fires when the phone
218
+ * sends WILL_DISCONNECT, or when this session calls `disconnect()`
219
+ * locally. Handlers run synchronously and may issue one final
220
+ * `sendOneShot`/`sendRequest` (e.g. `display.render([])`); async work won't complete
221
+ * before the socket closes.
222
+ */
223
+ beforeDisconnect: (reason: string) => void
224
+ disconnect: (reason: string) => void
225
+ visibility: (v: MiniappVisibility) => void
226
+ capabilities: (cap: GlassesCapabilities | null) => void
227
+ colorScheme: (scheme: MiniappColorScheme) => void
228
+ permissions: (perms: PermissionRecord) => void
229
+ speakerState: (event: import("./modules/speaker").SpeakerStateEvent) => void
230
+ auth: (auth: MiniappAuthState) => void
231
+ }
232
+
233
+ // The default preserves the pre-channel-registry behavior for code that creates
234
+ // or accepts a bare MiniappSession. `registerMiniapp<Channels>` supplies the
235
+ // concrete mapping for scaffolded miniapps.
236
+
237
+ export class MiniappSession<TChannels extends object = any> {
238
+ public readonly auth: AuthModule
239
+ public readonly display: DisplayManager
240
+ /**
241
+ * Internal subscription registry + escape hatch.
242
+ *
243
+ * Domain modules (`session.mic`, `session.input`, etc.) are the canonical
244
+ * surface for typed event subscriptions. `events.subscribe(...)` remains as
245
+ * a forward-compat escape hatch for new event types not yet wrapped on a
246
+ * domain module.
247
+ */
248
+ public readonly events: EventManager
249
+ public readonly speaker: SpeakerModule
250
+ public readonly camera: CameraModule
251
+ public readonly cloud: CloudModule
252
+ public readonly dashboard: DashboardAPI
253
+ public readonly glasses: GlassesModule
254
+ public readonly heading: HeadingModule
255
+ public readonly imu: ImuModule
256
+ public readonly input: InputModule
257
+ /**
258
+ * User-supplied provider API keys (Settings > API Keys). Requires the
259
+ * API_KEYS manifest permission; only an allowlisted provider set is
260
+ * readable (anthropic / gemini / elevenlabs / mapbox — never soniox).
261
+ */
262
+ /**
263
+ * Platform LLM completions. Requires the AI manifest permission. The
264
+ * platform picks and pays for the provider, or uses the user's own key when
265
+ * they've saved one.
266
+ */
267
+ public readonly ai: AiModule
268
+ public readonly keys: KeysModule
269
+ public readonly led: LedModule
270
+ public readonly location: LocationModule
271
+ /**
272
+ * The user's shared, phone-local memory timeline (transcript lines + notes +
273
+ * hourly summaries). Host-level and user-scoped — every miniapp declaring
274
+ * the MEMORY manifest permission shares the same store, and the user
275
+ * manages it in Settings > Memory. Requires the MEMORY permission.
276
+ */
277
+ public readonly memory: MemoryModule
278
+ /**
279
+ * Resolve enrolled-speaker ids (`spk_…` from TranscriptionData.speakerId)
280
+ * to the user's display names for them. Requires the SPEAKERS manifest
281
+ * permission — names are on-device PII behind an explicit gate. (Not to be
282
+ * confused with `session.speaker`, the audio-output module.)
283
+ */
284
+ public readonly speakers: SpeakersModule
285
+ public readonly mic: MicModule
286
+ public readonly navigation: NavigationModule
287
+ public readonly permissions: PermissionsModule
288
+ public readonly phone: PhoneModule
289
+ public readonly storage: SimpleStorage
290
+ /**
291
+ * Phone-local persistent BINARY storage (`session.blob`) — the binary
292
+ * counterpart to `session.storage`. Files on disk, scoped to this miniapp.
293
+ * Writes/reads are chunked so large payloads (e.g. captured audio fed in via
294
+ * `session.mic.onAudioChunk`) never cross the bridge in one message.
295
+ */
296
+ public readonly blob: BlobModule
297
+ public readonly stream: StreamModule
298
+ public readonly system: SystemModule
299
+ public readonly transcription: TranscriptionModule
300
+ public readonly translation: TranslationModule
301
+ /**
302
+ * UI message bus to the bound WebView (when one is open).
303
+ * Background-only API surface; mirrors the WebView's `glassly` global
304
+ * with inverted buffering policy (background drops when no WebView is
305
+ * bound; the WebView buffers until ready).
306
+ */
307
+ public readonly ui: UIModule<TChannels>
308
+ /**
309
+ * Inter-miniapp lifecycle + discovery (list / start / stop). SYSTEM-only —
310
+ * calls reject with NOT_PERMITTED unless this miniapp is a system app.
311
+ */
312
+ public readonly miniapps: MiniappsModule
313
+ /**
314
+ * Inter-miniapp action layer. `invoke` (SYSTEM-only) calls another miniapp's
315
+ * declared action; `handle` (open to all) exposes one of your own.
316
+ */
317
+ public readonly actions: ActionsModule
318
+
319
+ /** Phone-declared glasses capabilities. Null until CONNECT_ACK arrives. */
320
+ public capabilities: GlassesCapabilities | null = null
321
+ public userId = ""
322
+ public packageName = ""
323
+ public visibility: MiniappVisibility = "foreground"
324
+ /** Host color scheme. Seeded from window.Glassly, updated via session events. */
325
+ public colorScheme: MiniappColorScheme = "light"
326
+
327
+ /** True after CONNECT_ACK. Observe with waitForReady() or the "ready" event. */
328
+ public ready = false
329
+
330
+ private readonly transport: Transport
331
+ private readonly connectTimeoutMs: number
332
+ private readonly emitter = new EventEmitter<SessionEmitterEvents>()
333
+ private authState: MiniappAuthState | null = null
334
+ private readonly authWaiters = new Set<{
335
+ minTtlMs: number
336
+ resolve: (auth: MiniappAuthState) => void
337
+ reject: (error: Error) => void
338
+ timer: ReturnType<typeof setTimeout>
339
+ }>()
340
+ private authRefreshPromise: Promise<MiniappAuthState | null> | null = null
341
+
342
+ /**
343
+ * Outbound queue for anything sent before CONNECT_ACK. Flushed in FIFO order
344
+ * once the phone responds with CONNECT_ACK.
345
+ */
346
+ private readonly outboundQueue: string[] = []
347
+ private readonly pendingRequests = new Map<string, PendingRequest>()
348
+ private connectPromise: Promise<void> | null = null
349
+ private disposed = false
350
+
351
+ /** Manifest-declared permission cache. Updated on CONNECT_ACK / PERMISSIONS_UPDATE. */
352
+ private _permissions: PermissionRecord = {
353
+ location: false,
354
+ microphone: false,
355
+ camera: false,
356
+ notifications: false,
357
+ calendar: false,
358
+ apiKeys: false,
359
+ memory: false,
360
+ ai: false,
361
+ speakers: false,
362
+ }
363
+
364
+ constructor(options: MiniappSessionOptions = {}) {
365
+ this.transport = createTransport(options)
366
+ this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS
367
+
368
+ const injected = getGlasslyGlobals()
369
+ this.packageName = options.packageName ?? injected.packageName ?? ""
370
+ if (injected.colorScheme === "light" || injected.colorScheme === "dark") {
371
+ this.colorScheme = injected.colorScheme
372
+ }
373
+
374
+ this.auth = new AuthModule(this)
375
+ this.events = new EventManager(this)
376
+ this.speaker = new SpeakerModule(this)
377
+ this.camera = new CameraModule(this)
378
+ this.cloud = new CloudModule(this)
379
+ this.dashboard = new DashboardAPI(this)
380
+ this.display = new DisplayManager(this)
381
+ this.glasses = new GlassesModule(this)
382
+ this.heading = new HeadingModule(this)
383
+ this.imu = new ImuModule(this)
384
+ this.input = new InputModule(this)
385
+ this.ai = new AiModule(this)
386
+ this.keys = new KeysModule(this)
387
+ this.led = new LedModule(this)
388
+ this.location = new LocationModule(this)
389
+ this.memory = new MemoryModule(this)
390
+ this.speakers = new SpeakersModule(this)
391
+ this.mic = new MicModule(this)
392
+ this.navigation = new NavigationModule(this)
393
+ this.permissions = new PermissionsModule(this)
394
+ this.phone = new PhoneModule(this)
395
+ this.storage = new SimpleStorage(this)
396
+ this.blob = new BlobModule(this)
397
+ this.stream = new StreamModule(this)
398
+ this.system = new SystemModule(this)
399
+ this.transcription = new TranscriptionModule(this)
400
+ this.translation = new TranslationModule(this)
401
+ this.ui = new UIModuleImpl<TChannels>(this)
402
+ this.miniapps = new MiniappsModule(this)
403
+ this.actions = new ActionsModule(this)
404
+ }
405
+
406
+ /**
407
+ * @internal — synchronous lookup against the cached manifest-declared
408
+ * permission record from CONNECT_ACK / PERMISSIONS_UPDATE. Domain modules
409
+ * use this to expose their `hasPermission` getters without going to the
410
+ * wire. Returns false until CONNECT_ACK arrives.
411
+ *
412
+ * `manifestKey` is the manifest's UPPER_CASE permission name
413
+ * (MICROPHONE, CAMERA, LOCATION, READ_NOTIFICATIONS, etc.). Maps to v3's
414
+ * lowercase canonical keys internally.
415
+ */
416
+ _hasManifestPermission(manifestKey: string): boolean {
417
+ const canonical = manifestKeyToCanonical(manifestKey)
418
+ if (!canonical) return false
419
+ return this._permissions[canonical] === true
420
+ }
421
+
422
+ /**
423
+ * @internal — read the current manifest-declared permission record.
424
+ * Powers session.permissions.getAll(). Returns a fresh shallow copy so
425
+ * callers can't mutate internal state.
426
+ */
427
+ _getPermissions(): PermissionRecord {
428
+ return {...this._permissions}
429
+ }
430
+
431
+ /** @internal — current miniapp-scoped backend auth, if the host provided one. */
432
+ _getAuth(): MiniappAuthState | null {
433
+ return this.authState ? {...this.authState} : null
434
+ }
435
+
436
+ /**
437
+ * @internal — wait for a scoped miniapp token. Used by session.auth; not part
438
+ * of the public SDK surface because authors should never manage wire events.
439
+ */
440
+ _waitForAuth(minTtlMs: number, timeoutMs = 10_000): Promise<MiniappAuthState> {
441
+ const current = this.authState
442
+ if (current && this.authHasTtl(current, minTtlMs)) {
443
+ return Promise.resolve({...current})
444
+ }
445
+
446
+ void this.requestAuthRefresh(minTtlMs)
447
+
448
+ return new Promise((resolve, reject) => {
449
+ const waiter = {
450
+ minTtlMs,
451
+ resolve,
452
+ reject,
453
+ timer: setTimeout(() => {
454
+ this.authWaiters.delete(waiter)
455
+ reject(new NotConnectedError("Miniapp auth token is not available"))
456
+ }, timeoutMs),
457
+ }
458
+ this.authWaiters.add(waiter)
459
+ })
460
+ }
461
+
462
+ /**
463
+ * @internal — subscribe to a raw stream type. Domain modules call this; it
464
+ * delegates to the EventManager registry. Underscore prefix signals "not
465
+ * part of the public SDK surface — use session.mic.onAudioChunk /
466
+ * session.transcription.on(...)
467
+ * etc. instead."
468
+ */
469
+ _subscribe(
470
+ streamType: string,
471
+ handler: (data: unknown) => void,
472
+ options: {forceLocal?: boolean} = {},
473
+ ): UnsubscribeFn {
474
+ return this.events.subscribe(streamType, handler, options)
475
+ }
476
+
477
+ // -------------------------------------------------------------------------
478
+ // Connection lifecycle
479
+ // -------------------------------------------------------------------------
480
+
481
+ /**
482
+ * Connect to LocalMiniappRuntime. Idempotent — calling multiple times
483
+ * returns the same Promise.
484
+ */
485
+ connect(): Promise<void> {
486
+ if (this.disposed) {
487
+ return Promise.reject(new NotConnectedError("MiniappSession was disposed"))
488
+ }
489
+ if (this.connectPromise) return this.connectPromise
490
+
491
+ // Register the readiness listener BEFORE any awaits so that a synchronous
492
+ // CONNECT_ACK delivery in tests (or the phone) can't race the subscription.
493
+ const readyPromise = new Promise<void>((resolve, reject) => {
494
+ const timer = setTimeout(() => {
495
+ const err = new Error("MiniappSession: CONNECT_ACK timeout")
496
+ this.failAllPending(new MiniappRequestFailure(MiniappErrorCode.NOT_CONNECTED, err.message))
497
+ this.emitter.emit("error", err)
498
+ reject(err)
499
+ }, this.connectTimeoutMs)
500
+
501
+ this.emitter.once("ready", () => {
502
+ clearTimeout(timer)
503
+ resolve()
504
+ })
505
+ this.emitter.once("error", (err) => {
506
+ clearTimeout(timer)
507
+ reject(err)
508
+ })
509
+ })
510
+
511
+ this.connectPromise = (async () => {
512
+ this.transport.onMessage((raw) => this.handleIncoming(raw))
513
+ this.transport.onDisconnect((reason) => this.handleTransportDisconnect(reason))
514
+ await this.transport.open()
515
+
516
+ const requestId = makeRequestId()
517
+ const connectPayload = {
518
+ type: MiniappRequestType.CONNECT,
519
+ packageName: this.packageName,
520
+ }
521
+ this.transport.send(serializeEnvelope({payload: connectPayload, requestId}))
522
+
523
+ await readyPromise
524
+ })()
525
+
526
+ return this.connectPromise
527
+ }
528
+
529
+ /** Resolves when `ready` becomes true, or rejects if connect failed. */
530
+ waitForReady(): Promise<void> {
531
+ if (this.ready) return Promise.resolve()
532
+ return this.connect()
533
+ }
534
+
535
+ isConnected(): boolean {
536
+ return this.ready && this.transport.isOpen()
537
+ }
538
+
539
+ disconnect(): void {
540
+ if (this.disposed) return
541
+ this.disposed = true
542
+ // Give listeners one synchronous chance to flush final messages
543
+ // (e.g. display.render([])) before we tear down the transport.
544
+ try {
545
+ this.emitter.emit("beforeDisconnect", "disconnect called")
546
+ } catch (err) {
547
+ // A throwing handler must not block teardown.
548
+ console.warn("[MiniappSession] beforeDisconnect handler threw:", err)
549
+ }
550
+ this.failAllPending(new MiniappRequestFailure(MiniappErrorCode.REQUEST_ABORTED, "Session disconnected"))
551
+ try {
552
+ this.transport.close()
553
+ } catch {
554
+ // ignore
555
+ }
556
+ this.ready = false
557
+ this.emitter.emit("disconnect", "disconnect called")
558
+ }
559
+
560
+ // -------------------------------------------------------------------------
561
+ // Outbound traffic — modules call these
562
+ // -------------------------------------------------------------------------
563
+
564
+ /** Send a fire-and-forget request that does not need a response. */
565
+ sendOneShot(payload: object): void {
566
+ const envelope: MiniappEnvelope = {payload}
567
+ this.enqueueOrSend(serializeEnvelope(envelope))
568
+ }
569
+
570
+ /**
571
+ * Send a request and get a Promise that resolves with the REQUEST_RESULT payload.
572
+ * Rejects with a MiniappRequestError if the phone returns an error result.
573
+ *
574
+ * `opts.timeoutMs` overrides the default request timeout. Pass `0` to disable
575
+ * it entirely for inherently long-running requests whose duration is unbounded
576
+ * (e.g. audio playback that resolves only when the clip finishes) — those still
577
+ * settle via REQUEST_RESULT or `failAllPending` on disconnect, so they can't
578
+ * leak. Most requests should keep the default ceiling.
579
+ */
580
+ sendRequest<TResult = unknown>(payload: object, opts?: {timeoutMs?: number}): Promise<TResult> {
581
+ if (this.disposed) {
582
+ return Promise.reject(new NotConnectedError())
583
+ }
584
+ const requestId = makeRequestId()
585
+ const envelope: MiniappEnvelope = {payload, requestId}
586
+ const timeoutMs = opts?.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS
587
+ return new Promise<TResult>((resolve, reject) => {
588
+ // Reject (and drop) the request if the host never sends a REQUEST_RESULT,
589
+ // so the promise can't hang forever. The REQUEST_RESULT / failAllPending
590
+ // paths clear this timer before settling. A non-positive timeout opts out
591
+ // (long-running requests rely on the host result / disconnect to settle).
592
+ const timer =
593
+ timeoutMs > 0
594
+ ? setTimeout(() => {
595
+ const pending = this.pendingRequests.get(requestId)
596
+ if (!pending) return
597
+ this.pendingRequests.delete(requestId)
598
+ pending.reject(
599
+ new MiniappRequestFailure(
600
+ MiniappErrorCode.ACTION_TIMEOUT,
601
+ "Request timed out waiting for a response from the host",
602
+ ),
603
+ )
604
+ }, timeoutMs)
605
+ : undefined
606
+ this.pendingRequests.set(requestId, {
607
+ requestId,
608
+ resolve: resolve as (v: unknown) => void,
609
+ reject,
610
+ timer,
611
+ })
612
+ this.enqueueOrSend(serializeEnvelope(envelope))
613
+ })
614
+ }
615
+
616
+ // -------------------------------------------------------------------------
617
+ // Event emitter — external API
618
+ // -------------------------------------------------------------------------
619
+
620
+ on<K extends keyof SessionEmitterEvents>(event: K, handler: SessionEmitterEvents[K]): () => void {
621
+ this.emitter.on(event, handler as (...args: unknown[]) => void)
622
+ return () => this.emitter.off(event, handler as (...args: unknown[]) => void)
623
+ }
624
+
625
+ off<K extends keyof SessionEmitterEvents>(event: K, handler: SessionEmitterEvents[K]): void {
626
+ this.emitter.off(event, handler as (...args: unknown[]) => void)
627
+ }
628
+
629
+ /**
630
+ * Last-chance hook before the transport closes. Fires either when the
631
+ * phone notifies the session of an imminent disconnect (~50ms grace
632
+ * window before the socket is torn down) or when this session's
633
+ * `disconnect()` is called locally. Use it to flush final cleanup
634
+ * messages — e.g. `display.render([])` — synchronously. Async work
635
+ * started here will not complete before the socket closes.
636
+ */
637
+ onBeforeDisconnect(handler: (reason: string) => void): () => void {
638
+ return this.on("beforeDisconnect", handler)
639
+ }
640
+
641
+ onVisibilityChange(handler: (v: MiniappVisibility) => void): () => void {
642
+ return this.on("visibility", handler)
643
+ }
644
+
645
+ onCapabilitiesChange(handler: (cap: GlassesCapabilities | null) => void): () => void {
646
+ return this.on("capabilities", handler)
647
+ }
648
+
649
+ onColorSchemeChange(handler: (scheme: MiniappColorScheme) => void): () => void {
650
+ return this.on("colorScheme", handler)
651
+ }
652
+
653
+ // -------------------------------------------------------------------------
654
+ // Internal — transport glue
655
+ // -------------------------------------------------------------------------
656
+
657
+ private enqueueOrSend(raw: string): void {
658
+ if (this.ready) {
659
+ try {
660
+ this.transport.send(raw)
661
+ } catch (err) {
662
+ // If send fails post-ready, treat as transport error.
663
+ this.emitter.emit("error", err as Error)
664
+ }
665
+ return
666
+ }
667
+ this.outboundQueue.push(raw)
668
+ }
669
+
670
+ private flushQueue(): void {
671
+ const queue = this.outboundQueue.splice(0)
672
+ for (const raw of queue) {
673
+ try {
674
+ this.transport.send(raw)
675
+ } catch (err) {
676
+ this.emitter.emit("error", err as Error)
677
+ }
678
+ }
679
+ }
680
+
681
+ private handleIncoming(raw: string): void {
682
+ const envelope = parseEnvelope(raw)
683
+ if (!envelope) return
684
+
685
+ const payload = envelope.payload as {type?: string} & Record<string, unknown>
686
+ const type = payload?.type
687
+
688
+ switch (type) {
689
+ case MiniappResponseType.CONNECT_ACK: {
690
+ const ack = payload as unknown as ConnectAckPayload
691
+ this.userId = ack.userId ?? ""
692
+ if (ack.packageName) this.packageName = ack.packageName
693
+ this.capabilities = ack.capabilities ?? null
694
+ if (ack.visibility) this.visibility = ack.visibility
695
+ if (ack.colorScheme === "light" || ack.colorScheme === "dark") {
696
+ this.colorScheme = ack.colorScheme
697
+ }
698
+ if (ack.auth) {
699
+ this.applyAuth(ack.auth)
700
+ if (!this.userId) this.userId = ack.auth.glasslyUserId
701
+ }
702
+ // Populate the manifest-declared permission cache. Older runtimes
703
+ // that don't send `permissions` leave the all-false default in place
704
+ // — `hasPermission` getters will simply return false.
705
+ if (ack.permissions) this.applyPermissions(ack.permissions)
706
+ this.ready = true
707
+ this.flushQueue()
708
+ this.emitter.emit("ready")
709
+ // Don't resolve request correlation here — CONNECT_ACK has no requestId.
710
+ return
711
+ }
712
+
713
+ case MiniappResponseType.AUTH_UPDATE: {
714
+ const next = (payload as unknown as AuthUpdatePayload).auth
715
+ if (next) {
716
+ this.applyAuth(next)
717
+ if (!this.userId) this.userId = next.glasslyUserId
718
+ }
719
+ return
720
+ }
721
+
722
+ case MiniappResponseType.PERMISSIONS_UPDATE: {
723
+ const next = payload.permissions as PermissionRecord | undefined
724
+ if (next) this.applyPermissions(next)
725
+ return
726
+ }
727
+
728
+ case MiniappResponseType.SPEAKER_STATE: {
729
+ const state = payload.state as "idle" | "loading" | "playing" | "stopped" | "error" | undefined
730
+ if (!state) return
731
+ const event = {
732
+ state,
733
+ errorCode: payload.errorCode as string | undefined,
734
+ errorMessage: payload.errorMessage as string | undefined,
735
+ durationMs: payload.durationMs as number | undefined,
736
+ }
737
+ this.speaker._applyState(event)
738
+ this.emitter.emit("speakerState", event)
739
+ return
740
+ }
741
+
742
+ case MiniappRequestType.PING: {
743
+ // Phone → miniapp keepalive ping. Auto-reply with PONG.
744
+ const pong: object = {type: MiniappResponseType.PONG}
745
+ const env: MiniappEnvelope = {
746
+ payload: pong,
747
+ ...(envelope.requestId ? {requestId: envelope.requestId} : {}),
748
+ }
749
+ try {
750
+ this.transport.send(serializeEnvelope(env))
751
+ } catch {
752
+ // Ignore; next ping will fail too and runtime will unregister.
753
+ }
754
+ return
755
+ }
756
+
757
+ case MiniappResponseType.EVENT: {
758
+ const streamType = payload.streamType as string | undefined
759
+ if (!streamType) return
760
+ this.events._forwardEvent(
761
+ streamType,
762
+ payload.data,
763
+ payload.transcriptionRoute as TranscriptionEventRoute | undefined,
764
+ )
765
+ return
766
+ }
767
+
768
+ case MiniappResponseType.ACTION_CALL: {
769
+ // Another miniapp invoked one of our declared actions. Route to the
770
+ // registered handler (or buffer briefly for one to register). The SDK
771
+ // replies with an ACTION_RESULT request keyed by callId.
772
+ const callId = payload.callId as string | undefined
773
+ const actionId = payload.actionId as string | undefined
774
+ if (!callId || !actionId) return
775
+ const params = (payload.params as Record<string, unknown> | undefined) ?? {}
776
+ const callerPackageName = (payload.callerPackageName as string | undefined) ?? ""
777
+ this.actions._deliver(callId, actionId, params, {callerPackageName})
778
+ return
779
+ }
780
+
781
+ case MiniappResponseType.CAPABILITIES_UPDATE: {
782
+ const cap = (payload.capabilities as GlassesCapabilities | null) ?? null
783
+ this.capabilities = cap
784
+ this.emitter.emit("capabilities", cap)
785
+ return
786
+ }
787
+
788
+ case MiniappResponseType.VISIBILITY_CHANGE: {
789
+ const next = payload.visibility as MiniappVisibility | undefined
790
+ if (next === "foreground" || next === "background") {
791
+ this.visibility = next
792
+ this.emitter.emit("visibility", next)
793
+ }
794
+ return
795
+ }
796
+
797
+ case MiniappResponseType.COLOR_SCHEME_CHANGE: {
798
+ const next = payload.colorScheme as MiniappColorScheme | undefined
799
+ if (next === "light" || next === "dark") {
800
+ this.colorScheme = next
801
+ this.emitter.emit("colorScheme", next)
802
+ }
803
+ return
804
+ }
805
+
806
+ case MiniappResponseType.REQUEST_RESULT: {
807
+ const requestId = envelope.requestId
808
+ if (!requestId) return
809
+ const pending = this.pendingRequests.get(requestId)
810
+ if (!pending) return
811
+ this.pendingRequests.delete(requestId)
812
+ if (pending.timer) clearTimeout(pending.timer)
813
+ if (payload.ok === false) {
814
+ const wire = payload.error as MiniappRequestError | undefined
815
+ pending.reject(
816
+ new MiniappRequestFailure(wire?.code ?? MiniappErrorCode.INTERNAL, wire?.message ?? "Unknown error"),
817
+ )
818
+ } else {
819
+ pending.resolve(payload.data ?? null)
820
+ }
821
+ return
822
+ }
823
+
824
+ case MiniappResponseType.WILL_DISCONNECT: {
825
+ const reason = (payload.reason as string | undefined) ?? "phone unregistering"
826
+ try {
827
+ this.emitter.emit("beforeDisconnect", reason)
828
+ } catch (err) {
829
+ console.warn("[MiniappSession] beforeDisconnect handler threw:", err)
830
+ }
831
+ return
832
+ }
833
+
834
+ case MiniappResponseType.ERROR: {
835
+ const err = new Error((payload.message as string | undefined) ?? "MiniappSession error")
836
+ this.emitter.emit("error", err)
837
+ return
838
+ }
839
+
840
+ default:
841
+ // Unknown type — drop silently. Forward-compat.
842
+ return
843
+ }
844
+ }
845
+
846
+ private handleTransportDisconnect(reason: string): void {
847
+ this.ready = false
848
+ this.failAllPending(new MiniappRequestFailure(MiniappErrorCode.NOT_CONNECTED, `Transport disconnected: ${reason}`))
849
+ this.emitter.emit("disconnect", reason)
850
+ }
851
+
852
+ private failAllPending(error: MiniappRequestFailure): void {
853
+ for (const pending of this.pendingRequests.values()) {
854
+ if (pending.timer) clearTimeout(pending.timer)
855
+ pending.reject(error)
856
+ }
857
+ this.pendingRequests.clear()
858
+ // Auth waiters live outside pendingRequests — they're resolved by an
859
+ // AUTH_UPDATE push, not a correlated REQUEST_RESULT — so a transport drop,
860
+ // dispose, or CONNECT_ACK timeout must reject them here too. Otherwise an
861
+ // in-flight session.auth.getToken()/fetch() hangs until its 10s waiter
862
+ // timeout instead of failing fast with the disconnect error.
863
+ for (const waiter of Array.from(this.authWaiters)) {
864
+ clearTimeout(waiter.timer)
865
+ this.authWaiters.delete(waiter)
866
+ waiter.reject(new NotConnectedError(error.message))
867
+ }
868
+ }
869
+
870
+ private authHasTtl(auth: MiniappAuthState, minTtlMs: number): boolean {
871
+ return auth.expiresAt - Date.now() > minTtlMs
872
+ }
873
+
874
+ private requestAuthRefresh(minTtlMs: number): Promise<MiniappAuthState | null> {
875
+ if (this.authRefreshPromise) return this.authRefreshPromise
876
+ if (!this.ready || !this.transport.isOpen()) return Promise.resolve(null)
877
+
878
+ this.authRefreshPromise = this.sendRequest<AuthRefreshResult>({
879
+ type: MiniappRequestType.AUTH_REFRESH,
880
+ minTtlMs,
881
+ })
882
+ .then((result) => {
883
+ const auth = result?.auth
884
+ if (auth) this.applyAuth(auth)
885
+ return auth ?? null
886
+ })
887
+ .catch((err) => {
888
+ console.warn("[MiniappSession] auth refresh failed:", err)
889
+ return null
890
+ })
891
+ .finally(() => {
892
+ this.authRefreshPromise = null
893
+ })
894
+
895
+ return this.authRefreshPromise
896
+ }
897
+
898
+ private applyAuth(next: MiniappAuthState): void {
899
+ this.authState = {...next}
900
+ for (const waiter of Array.from(this.authWaiters)) {
901
+ if (!this.authHasTtl(next, waiter.minTtlMs)) continue
902
+ clearTimeout(waiter.timer)
903
+ this.authWaiters.delete(waiter)
904
+ waiter.resolve({...next})
905
+ }
906
+ this.emitter.emit("auth", {...next})
907
+ }
908
+
909
+ /**
910
+ * Update the cached permission record. Idempotent: emits "permissions"
911
+ * only when the record actually changed. Sanitizes incoming objects to
912
+ * the v3 PermissionType union.
913
+ */
914
+ private applyPermissions(next: Partial<PermissionRecord>): void {
915
+ let changed = false
916
+ const updated: PermissionRecord = {...this._permissions}
917
+ for (const k of ALL_PERMISSION_TYPES) {
918
+ const v = next[k] === true
919
+ if (updated[k] !== v) {
920
+ updated[k] = v
921
+ changed = true
922
+ }
923
+ }
924
+ if (changed) {
925
+ this._permissions = updated
926
+ this.emitter.emit("permissions", {...updated})
927
+ }
928
+ }
929
+ }
930
+
931
+ /**
932
+ * Map a manifest UPPER_CASE permission name to the lowercase canonical key
933
+ * used by `session.permissions`. Returns null for unknown manifest keys.
934
+ *
935
+ * BACKGROUND_LOCATION + POST_NOTIFICATIONS map onto the same canonical keys
936
+ * as their non-suffixed counterparts (location / notifications) since
937
+ * `has()` is "do I have *any* form of this permission declared".
938
+ */
939
+ function manifestKeyToCanonical(manifestKey: string): PermissionType | null {
940
+ switch (manifestKey.toUpperCase()) {
941
+ case "MICROPHONE":
942
+ return "microphone"
943
+ case "CAMERA":
944
+ return "camera"
945
+ case "LOCATION":
946
+ case "BACKGROUND_LOCATION":
947
+ return "location"
948
+ case "READ_NOTIFICATIONS":
949
+ case "POST_NOTIFICATIONS":
950
+ return "notifications"
951
+ case "CALENDAR":
952
+ return "calendar"
953
+ case "API_KEYS":
954
+ return "apiKeys"
955
+ case "MEMORY":
956
+ return "memory"
957
+ case "AI":
958
+ return "ai"
959
+ case "SPEAKERS":
960
+ return "speakers"
961
+ default:
962
+ return null
963
+ }
964
+ }
965
+
966
+ /** @internal — for the permissions module's onUpdate plumbing. */
967
+ export function _allPermissionTypes(): readonly PermissionType[] {
968
+ return ALL_PERMISSION_TYPES
969
+ }