@glassly/miniapp 0.1.0-dev.26

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 +24 -0
  23. package/dist/hardware.d.ts.map +1 -0
  24. package/dist/hardware.js +20 -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 +174 -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 +50 -0
  83. package/dist/modules/input.d.ts.map +1 -0
  84. package/dist/modules/input.js +70 -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 +390 -0
  171. package/dist/protocol.d.ts.map +1 -0
  172. package/dist/protocol.js +419 -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 +88 -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 +25 -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 +214 -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 +87 -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 +482 -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
@@ -0,0 +1,341 @@
1
+ /**
2
+ * @fileoverview BlobModule — phone-local persistent BINARY storage, keyed and
3
+ * scoped to (userId, packageName). The binary sibling of `session.storage`:
4
+ * where storage holds small strings, blob holds arbitrary bytes (PDFs, EPUBs,
5
+ * audio, video, model files, caches) as files on the phone.
6
+ *
7
+ * It deliberately mirrors `session.storage`'s shape — `set` / `get` / `delete` /
8
+ * `keys` / `has` / `clear` — so it's instantly familiar, plus the things files
9
+ * (and not strings) need:
10
+ * - `setFromUrl(key, url)` — the host downloads a URL straight to disk
11
+ * (like the web Cache API's `cache.add`, iOS `URLSession` download task, or
12
+ * Android `DownloadManager`). Bytes never cross the bridge.
13
+ * - `importFile()` — opens the OS file picker and stores the chosen file.
14
+ * Bytes never cross the bridge.
15
+ * - `share(key)` — hands the file to the OS share sheet.
16
+ * - `createWriteStream` / `createReadStream` — for streaming huge payloads
17
+ * (named like Node's `fs.createWriteStream`).
18
+ *
19
+ * BACKGROUND-ONLY. Binary doesn't belong in the WebView. A blob's `uri` is a
20
+ * `file://` path in the app's private storage — fine for `session.speaker.play`,
21
+ * `blob.share`, or another host capability, but a WebView generally can't load
22
+ * it directly (route rendering through a host viewer for now).
23
+ *
24
+ * Transfer model: the bridge moves JSON strings through a per-miniapp JS engine
25
+ * (JSC/QuickJS) with a watchdog, so in-JS writes/reads are CHUNKED (≤ ~1 MB raw
26
+ * per call). Prefer `setFromUrl` / `importFile` when you can — they keep the
27
+ * bytes entirely host-side.
28
+ */
29
+
30
+ import {MiniappRequestType} from "../protocol"
31
+ import {MiniappSession} from "../session"
32
+ import {base64ToBytes, bytesToBase64, toUint8Array} from "./base64"
33
+
34
+ /** Metadata for one stored blob. `uri` is a `file://` path. */
35
+ export interface BlobMeta {
36
+ /** The key it's stored under (caller-chosen). */
37
+ key: string
38
+ /** Optional display/file name (e.g. the original filename for an import). */
39
+ name?: string
40
+ /** MIME type, e.g. "application/pdf". */
41
+ mimeType: string
42
+ /** Size on disk in bytes. */
43
+ bytes: number
44
+ /** Epoch ms first written. */
45
+ createdAt: number
46
+ /** Epoch ms last written. */
47
+ updatedAt: number
48
+ /** Content md5 (lowercase hex), computed host-side (skipped for very large blobs). */
49
+ md5?: string
50
+ /** `file://` URI. Feed to `session.speaker.play`, `blob.share`, etc. */
51
+ uri: string
52
+ /** App-defined metadata persisted with the blob. */
53
+ meta?: Record<string, string | number | boolean>
54
+ }
55
+
56
+ export interface BlobSetOptions {
57
+ /** Default "application/octet-stream". */
58
+ mimeType?: string
59
+ name?: string
60
+ meta?: Record<string, string | number | boolean>
61
+ }
62
+
63
+ export interface BlobSetFromUrlOptions extends BlobSetOptions {
64
+ /** Request headers (e.g. an Authorization token) for the download. */
65
+ headers?: Record<string, string>
66
+ }
67
+
68
+ export interface BlobImportOptions {
69
+ /** Key to store under. Default: a generated key. */
70
+ key?: string
71
+ /** Restrict the picker to a MIME type, e.g. "application/pdf". */
72
+ mimeType?: string
73
+ meta?: Record<string, string | number | boolean>
74
+ }
75
+
76
+ /** Raw bytes per chunked write. ~1 MB raw → ~1.34 MB base64, under the watchdog. */
77
+ export const BLOB_WRITE_CHUNK_BYTES = 1024 * 1024
78
+ const BLOB_WRITE_CHUNK_B64 = Math.floor(BLOB_WRITE_CHUNK_BYTES / 3) * 4
79
+ /** Max bytes `bytes()` will buffer before throwing — read large blobs as a stream. */
80
+ export const BLOB_READ_ALL_MAX_BYTES = 32 * 1024 * 1024
81
+
82
+ /**
83
+ * Streaming writer (`fs.createWriteStream`-like). Call `write`/`writeBase64` as
84
+ * many times as you like (each is auto-split into bridge-safe chunks), then
85
+ * `close()` to publish, or `abort()` to discard the partial blob.
86
+ */
87
+ export class BlobWriter {
88
+ private settled = false
89
+
90
+ constructor(
91
+ private readonly session: MiniappSession,
92
+ readonly key: string,
93
+ ) {}
94
+
95
+ /** Append raw bytes. Auto-chunked. */
96
+ async write(chunk: Uint8Array | ArrayBuffer): Promise<void> {
97
+ this.assertOpen()
98
+ const bytes = toUint8Array(chunk)
99
+ for (let off = 0; off < bytes.length; off += BLOB_WRITE_CHUNK_BYTES) {
100
+ await this.send(bytesToBase64(bytes.subarray(off, off + BLOB_WRITE_CHUNK_BYTES)))
101
+ }
102
+ }
103
+
104
+ /** Append already-base64-encoded bytes (e.g. straight from `mic.onAudioChunk`). */
105
+ async writeBase64(b64: string): Promise<void> {
106
+ this.assertOpen()
107
+ // Slice on 4-char boundaries so each chunk is whole base64 groups.
108
+ for (let off = 0; off < b64.length; off += BLOB_WRITE_CHUNK_B64) {
109
+ await this.send(b64.slice(off, off + BLOB_WRITE_CHUNK_B64))
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Overwrite bytes at a fixed offset within the not-yet-closed blob (a seek
115
+ * write). Must stay within already-written bytes; does not grow the blob.
116
+ * Advanced — used e.g. to patch a container header on finalize.
117
+ */
118
+ async writeAt(offset: number, chunk: Uint8Array | ArrayBuffer): Promise<void> {
119
+ this.assertOpen()
120
+ await this.session.sendRequest<{bytesWritten: number}>({
121
+ type: MiniappRequestType.BLOB_WRITE,
122
+ key: this.key,
123
+ offset,
124
+ base64: bytesToBase64(toUint8Array(chunk)),
125
+ })
126
+ }
127
+
128
+ /** Finalize and return the blob's metadata. Optional `meta` merges into the record. */
129
+ async close(meta?: Record<string, string | number | boolean>): Promise<BlobMeta> {
130
+ this.assertOpen()
131
+ this.settled = true
132
+ return this.session.sendRequest<BlobMeta>({type: MiniappRequestType.BLOB_COMMIT, key: this.key, meta})
133
+ }
134
+
135
+ /** Discard the partial blob. Idempotent. */
136
+ async abort(): Promise<void> {
137
+ if (this.settled) return
138
+ this.settled = true
139
+ await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_ABORT, key: this.key})
140
+ }
141
+
142
+ private assertOpen(): void {
143
+ if (this.settled) throw new Error("BlobWriter is already closed/aborted")
144
+ }
145
+
146
+ private send(base64: string): Promise<{bytesWritten: number}> {
147
+ return this.session.sendRequest<{bytesWritten: number}>({
148
+ type: MiniappRequestType.BLOB_WRITE,
149
+ key: this.key,
150
+ base64,
151
+ })
152
+ }
153
+ }
154
+
155
+ /** Streaming reader (`fs.createReadStream`-like). */
156
+ export class BlobReader {
157
+ private closed = false
158
+
159
+ constructor(
160
+ private readonly session: MiniappSession,
161
+ readonly handle: string,
162
+ readonly meta: BlobMeta,
163
+ ) {}
164
+
165
+ /** Read up to `maxBytes` (default one chunk). `done` is true at end of file. */
166
+ async read(maxBytes = BLOB_WRITE_CHUNK_BYTES): Promise<{bytes: Uint8Array; done: boolean}> {
167
+ if (this.closed) throw new Error("BlobReader is closed")
168
+ const res = await this.session.sendRequest<{base64: string; done: boolean}>({
169
+ type: MiniappRequestType.BLOB_READ,
170
+ handle: this.handle,
171
+ maxBytes,
172
+ })
173
+ return {bytes: base64ToBytes(res?.base64 ?? ""), done: !!res?.done}
174
+ }
175
+
176
+ async close(): Promise<void> {
177
+ if (this.closed) return
178
+ this.closed = true
179
+ await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_CLOSE_READ, handle: this.handle})
180
+ }
181
+ }
182
+
183
+ export class BlobModule {
184
+ constructor(private readonly session: MiniappSession) {}
185
+
186
+ // ── write ────────────────────────────────────────────────────────────────
187
+
188
+ /** Store bytes under `key`. `data` may be a Uint8Array/ArrayBuffer or a base64 string. */
189
+ async set(key: string, data: Uint8Array | ArrayBuffer | string, opts: BlobSetOptions = {}): Promise<BlobMeta> {
190
+ const writer = await this.createWriteStream(key, opts)
191
+ try {
192
+ if (typeof data === "string") await writer.writeBase64(data)
193
+ else await writer.write(data)
194
+ return await writer.close()
195
+ } catch (err) {
196
+ await writer.abort().catch(() => {})
197
+ throw err
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Download `url` straight into a blob under `key`, host-side. The bytes never
203
+ * cross the bridge (like the web Cache API's `cache.add`). `opts.headers` lets
204
+ * you pass auth. Resolves to the stored blob's metadata.
205
+ */
206
+ setFromUrl(key: string, url: string, opts: BlobSetFromUrlOptions = {}): Promise<BlobMeta> {
207
+ return this.session.sendRequest<BlobMeta>({
208
+ type: MiniappRequestType.BLOB_SET_FROM_URL,
209
+ key,
210
+ url,
211
+ mimeType: opts.mimeType,
212
+ name: opts.name,
213
+ headers: opts.headers,
214
+ meta: opts.meta,
215
+ })
216
+ }
217
+
218
+ /**
219
+ * Open the OS file picker and store the chosen file as a blob, host-side.
220
+ * Resolves to the blob's metadata, or `null` if the user cancelled.
221
+ */
222
+ importFile(opts: BlobImportOptions = {}): Promise<BlobMeta | null> {
223
+ return this.session.sendRequest<BlobMeta | null>({
224
+ type: MiniappRequestType.BLOB_IMPORT,
225
+ key: opts.key,
226
+ mimeType: opts.mimeType,
227
+ meta: opts.meta,
228
+ })
229
+ }
230
+
231
+ /** Open a streaming writer for `key` (for large/streamed payloads). */
232
+ async createWriteStream(key: string, opts: BlobSetOptions = {}): Promise<BlobWriter> {
233
+ const res = await this.session.sendRequest<{key: string}>({
234
+ type: MiniappRequestType.BLOB_CREATE,
235
+ key,
236
+ mimeType: opts.mimeType,
237
+ name: opts.name,
238
+ meta: opts.meta,
239
+ })
240
+ return new BlobWriter(this.session, res.key)
241
+ }
242
+
243
+ // ── read ─────────────────────────────────────────────────────────────────
244
+
245
+ /** Metadata (incl. the `file://` uri) for `key`, or null if absent. */
246
+ get(key: string): Promise<BlobMeta | null> {
247
+ return this.session.sendRequest<BlobMeta | null>({type: MiniappRequestType.BLOB_GET, key})
248
+ }
249
+
250
+ /** Alias of `get` — the file-stat verb. */
251
+ stat(key: string): Promise<BlobMeta | null> {
252
+ return this.get(key)
253
+ }
254
+
255
+ /** True iff a blob is stored under `key`. */
256
+ async has(key: string): Promise<boolean> {
257
+ return (await this.get(key)) !== null
258
+ }
259
+
260
+ /** Every key this miniapp has stored, newest first. */
261
+ async keys(): Promise<string[]> {
262
+ return (await this.list()).map((m) => m.key)
263
+ }
264
+
265
+ /** Every blob this miniapp owns, newest first. */
266
+ async list(): Promise<BlobMeta[]> {
267
+ const res = await this.session.sendRequest<{blobs: BlobMeta[]}>({type: MiniappRequestType.BLOB_LIST})
268
+ return res?.blobs ?? []
269
+ }
270
+
271
+ /** Open a streaming reader. Prefer `get(key).uri` when you just need to play/share it. */
272
+ async createReadStream(key: string): Promise<BlobReader> {
273
+ const res = await this.session.sendRequest<{handle: string; meta: BlobMeta}>({
274
+ type: MiniappRequestType.BLOB_OPEN_READ,
275
+ key,
276
+ })
277
+ return new BlobReader(this.session, res.handle, res.meta)
278
+ }
279
+
280
+ /** Read a whole blob into memory as bytes, or null if absent. Throws past the cap — stream big ones. */
281
+ async bytes(key: string): Promise<Uint8Array | null> {
282
+ let reader: BlobReader
283
+ try {
284
+ reader = await this.createReadStream(key)
285
+ } catch {
286
+ return null
287
+ }
288
+ const parts: Uint8Array[] = []
289
+ let total = 0
290
+ try {
291
+ for (;;) {
292
+ const {bytes, done} = await reader.read()
293
+ if (bytes.length) {
294
+ total += bytes.length
295
+ if (total > BLOB_READ_ALL_MAX_BYTES) {
296
+ throw new Error(`Blob "${key}" exceeds the in-memory read cap — stream it with createReadStream()`)
297
+ }
298
+ parts.push(bytes)
299
+ }
300
+ if (done) break
301
+ }
302
+ } finally {
303
+ await reader.close().catch(() => {})
304
+ }
305
+ const out = new Uint8Array(total)
306
+ let at = 0
307
+ for (const p of parts) {
308
+ out.set(p, at)
309
+ at += p.length
310
+ }
311
+ return out
312
+ }
313
+
314
+ // ── manage ───────────────────────────────────────────────────────────────
315
+
316
+ /** Per-app usage + the quota ceiling, in bytes. */
317
+ usage(): Promise<{bytes: number; count: number; quotaBytes: number}> {
318
+ return this.session.sendRequest<{bytes: number; count: number; quotaBytes: number}>({
319
+ type: MiniappRequestType.BLOB_USAGE,
320
+ })
321
+ }
322
+
323
+ /** Delete the blob under `key`. No-op if absent. */
324
+ async delete(key: string): Promise<void> {
325
+ await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_DELETE, key})
326
+ }
327
+
328
+ /** Delete every blob this miniapp owns. */
329
+ async clear(): Promise<void> {
330
+ await this.session.sendRequest<void>({type: MiniappRequestType.BLOB_CLEAR})
331
+ }
332
+
333
+ /** Share the blob via the OS share sheet (host shares from disk — no bytes cross the bridge). */
334
+ async share(key: string): Promise<{success: boolean; cancelled?: boolean}> {
335
+ const res = await this.session.sendRequest<{success: boolean; cancelled?: boolean}>({
336
+ type: MiniappRequestType.BLOB_SHARE,
337
+ key,
338
+ })
339
+ return res ?? {success: false}
340
+ }
341
+ }
@@ -0,0 +1,258 @@
1
+ /**
2
+ * @fileoverview CameraModule — glasses camera control and photo capture.
3
+ */
4
+
5
+ import {MiniappRequestType} from "../protocol"
6
+ import {MiniappSession} from "../session"
7
+
8
+ export type CameraRoiPosition = "center" | "bottom" | "top"
9
+ export type CameraFovPreset = "narrow" | "standard" | "wide"
10
+
11
+ export type CameraFovRequest =
12
+ | {
13
+ /** Horizontal FOV, degrees. */
14
+ fov: number
15
+ /** Crop/region position: center, bottom, or top. Defaults to center. */
16
+ roiPosition?: CameraRoiPosition
17
+ }
18
+ | {
19
+ /** Named FOV preset. Presets use center ROI. */
20
+ preset: CameraFovPreset
21
+ }
22
+
23
+ export type SetCameraFovOptions = CameraFovRequest
24
+
25
+ export interface CameraFovResult {
26
+ requestId: string
27
+ fov: number
28
+ roiPosition: CameraRoiPosition
29
+ timestamp: number
30
+ }
31
+
32
+ export interface TakePhotoOptions {
33
+ size?: "low" | "medium" | "high" | "max"
34
+ /** Capture at maximum source quality and optimize BLE delivery for readable text. */
35
+ mode?: "photo" | "text"
36
+ /**
37
+ * Image delivery path. `auto` tries direct Wi-Fi upload and falls back to
38
+ * BLE; `direct` disables BLE fallback; `ble` always relays through the phone.
39
+ */
40
+ transferMethod?: "auto" | "direct" | "ble"
41
+ compress?: "none" | "low" | "medium" | "high"
42
+ sound?: boolean
43
+ saveToGallery?: boolean
44
+ /**
45
+ * Manual shutter / exposure time in nanoseconds. Omit (or pass undefined)
46
+ * to let the glasses auto-expose. Honored only on cameras that support
47
+ * manual exposure; ignored otherwise.
48
+ */
49
+ exposureTimeNs?: number
50
+ /** Sensor ISO for this capture only. Only used when `exposureTimeNs` enables manual exposure. */
51
+ iso?: number | null
52
+ /** After AE convergence, divide the metered exposure by this factor (scan mode). */
53
+ aeExposureDivisor?: number
54
+ /** Cap ISO after AE metering (scan mode). */
55
+ isoCap?: number
56
+ /** Request per-capture noise reduction. Sent on the wire; glasses may log `not_implemented`. */
57
+ noiseReduction?: boolean
58
+ /** Request per-capture edge enhancement. Sent on the wire; glasses may log `not_implemented`. */
59
+ edgeEnhancement?: boolean
60
+ /**
61
+ * ZSL buffering. Manual and scan stills force it off because fixed sensor controls take priority.
62
+ */
63
+ zsl?: boolean
64
+ /**
65
+ * MFNR still capture. Manual and scan stills force it off because fixed sensor controls take priority.
66
+ */
67
+ mfnr?: boolean
68
+ /** ISP digital gain hint. */
69
+ ispDigitalGain?: number
70
+ /** ISP analog gain hint. */
71
+ ispAnalogGain?: string
72
+ /**
73
+ * Override the SDK's default 60s request timeout for this capture only.
74
+ * Leave unset for a normal user-triggered capture (the full 60s ceiling is
75
+ * appropriate there). Callers that speculatively fire a capture and may
76
+ * abandon it if the result isn't needed (e.g. a short-lived non-visual
77
+ * assistant turn racing a capture against a ~1s latency cap) should pass a
78
+ * short value here so the abandoned `takePhoto()` promise settles quickly
79
+ * instead of sitting on the default 60s ceiling.
80
+ */
81
+ timeoutMs?: number
82
+ }
83
+
84
+ export interface PhotoTaken {
85
+ /** Request id that correlates ASG status, upload, and phone/cloud logs. */
86
+ requestId: string
87
+ photoUrl: string
88
+ mimeType: string
89
+ size: number
90
+ }
91
+
92
+ export interface WarmUpCameraOptions {
93
+ size?: "low" | "medium" | "high" | "max"
94
+ /** Match the upcoming capture mode so warm-up and capture share one ASG camera session. */
95
+ mode?: "photo" | "text"
96
+ exposureTimeNs?: number
97
+ /** Ready-state hold in milliseconds. Defaults to 15 seconds and is capped at 60 seconds. */
98
+ durationMs?: number
99
+ /** ZSL preview buffering for the warm-up session. */
100
+ zsl?: boolean
101
+ /** MFNR still capture for the warm-up session. */
102
+ mfnr?: boolean
103
+ }
104
+
105
+ export interface StartVideoRecordingOptions {
106
+ /** Video width in pixels. Omit to use the device's saved button-video default. */
107
+ width?: number
108
+ /** Video height in pixels. Omit to use the device's saved button-video default. */
109
+ height?: number
110
+ /**
111
+ * Frames per second. Lower frame rates keep the glasses cooler and produce
112
+ * smaller files — useful for long recordings where smooth motion isn't needed.
113
+ * Omit to use the device default.
114
+ */
115
+ fps?: number
116
+ /** Play start/stop capture sounds. Defaults to true. */
117
+ sound?: boolean
118
+ /** Keep the recording on the glasses after it finishes. Defaults to false. */
119
+ save?: boolean
120
+ }
121
+
122
+ export interface VideoRecordingStarted {
123
+ /** Identifier for this recording session; pass to {@link stopVideoRecording}. */
124
+ recordingId: string
125
+ }
126
+
127
+ export interface StopVideoRecordingOptions {
128
+ /**
129
+ * Upload the finished recording to this URL with a multipart POST (the file is
130
+ * the `video` part). The glasses upload it directly. Omit to keep the recording
131
+ * on the glasses instead.
132
+ */
133
+ uploadUrl?: string
134
+ /** Bearer token sent as the `Authorization` header on the upload, if your endpoint needs one. */
135
+ uploadAuthToken?: string
136
+ }
137
+
138
+ export class CameraModule {
139
+ constructor(private readonly session: MiniappSession) {}
140
+
141
+ /** True iff `CAMERA` is declared in the miniapp's manifest. */
142
+ get hasPermission(): boolean {
143
+ return this.session._hasManifestPermission("CAMERA")
144
+ }
145
+
146
+ /**
147
+ * Apply a session-owned camera FOV/ROI override on the glasses.
148
+ *
149
+ * Resolves after the ASG client reports that the setting was applied to camera
150
+ * hardware after the restart cooldown. The host restores the prior override or persistent base
151
+ * when this miniapp closes. Requires CAMERA permission declared in miniapp.json.
152
+ */
153
+ async setFov(request: CameraFovRequest): Promise<CameraFovResult> {
154
+ return this.session.sendRequest<CameraFovResult>({
155
+ type: MiniappRequestType.CAMERA_FOV,
156
+ ...request,
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Take a photo via the glasses camera. Returns a URL to the captured image.
162
+ * Requires CAMERA permission declared in miniapp.json.
163
+ *
164
+ * The photo is uploaded to cloud storage; the returned URL is a short-TTL
165
+ * (~30 minute) signed download URL. If the glasses don't have a camera,
166
+ * the phone-side handler rejects with an error. Check
167
+ * `session.capabilities.hasCamera` before calling.
168
+ */
169
+ async takePhoto(options: TakePhotoOptions = {}): Promise<PhotoTaken> {
170
+ return this.session.sendRequest<PhotoTaken>(
171
+ {
172
+ type: MiniappRequestType.PHOTO,
173
+ size: options.size ?? "medium",
174
+ mode: options.mode ?? "photo",
175
+ ...(options.transferMethod !== undefined ? {transferMethod: options.transferMethod} : {}),
176
+ compress: options.compress ?? "none",
177
+ sound: options.sound ?? true,
178
+ saveToGallery: options.saveToGallery ?? false,
179
+ exposureTimeNs: options.exposureTimeNs,
180
+ iso: options.iso,
181
+ aeExposureDivisor: options.aeExposureDivisor,
182
+ isoCap: options.isoCap,
183
+ noiseReduction: options.noiseReduction,
184
+ edgeEnhancement: options.edgeEnhancement,
185
+ zsl: options.zsl,
186
+ mfnr: options.mfnr,
187
+ ispDigitalGain: options.ispDigitalGain,
188
+ ispAnalogGain: options.ispAnalogGain,
189
+ },
190
+ options.timeoutMs != null ? {timeoutMs: options.timeoutMs} : undefined,
191
+ )
192
+ }
193
+
194
+ /**
195
+ * Pre-warm the glasses camera so the next takePhoto() is near-instant.
196
+ * The camera stays warm for ~durationMs (default 15s, maximum 60s); call warmUp() again to
197
+ * extend it. Warm with the same `size` you'll capture with — a mismatched size
198
+ * forces the camera to reconfigure and loses the speedup. Requires CAMERA
199
+ * permission in miniapp.json. Resolves once the camera reports ready. The host automatically
200
+ * releases this miniapp's request-owned lease when the miniapp closes.
201
+ *
202
+ * Warm-ups are serialized: only one runs at a time and none may start while a
203
+ * photo is being captured. If the camera is busy (a capture is in flight, or
204
+ * another warm-up is still opening) this rejects with `camera_busy` — retry
205
+ * shortly, or just call takePhoto() (it works regardless, only slower). The
206
+ * normal warmUp() → takePhoto() sequence never hits this.
207
+ */
208
+ async warmUp(options: WarmUpCameraOptions = {}): Promise<void> {
209
+ await this.session.sendRequest<void>({
210
+ type: MiniappRequestType.CAMERA_WARM_UP,
211
+ size: options.size ?? "medium",
212
+ mode: options.mode ?? "photo",
213
+ exposureTimeNs: options.exposureTimeNs,
214
+ durationMs: options.durationMs ?? 15000,
215
+ zsl: options.zsl,
216
+ mfnr: options.mfnr,
217
+ })
218
+ }
219
+
220
+ /**
221
+ * Start recording video on the glasses camera. Returns a `recordingId` to pass
222
+ * to {@link stopVideoRecording} after the glasses report that recording
223
+ * started. Requires CAMERA permission declared in miniapp.json. Check
224
+ * `session.capabilities.hasCamera` before calling.
225
+ *
226
+ * Resolution and frame rate are optional — omit them to use the device's saved
227
+ * button-video settings. Lowering `fps` (e.g. to 5) keeps the glasses cooler
228
+ * during long recordings.
229
+ */
230
+ async startVideoRecording(options: StartVideoRecordingOptions = {}): Promise<VideoRecordingStarted> {
231
+ return this.session.sendRequest<VideoRecordingStarted>({
232
+ type: MiniappRequestType.VIDEO_RECORDING_START,
233
+ width: options.width,
234
+ height: options.height,
235
+ fps: options.fps,
236
+ sound: options.sound ?? true,
237
+ save: options.save ?? false,
238
+ })
239
+ }
240
+
241
+ /**
242
+ * Stop an in-progress video recording started with {@link startVideoRecording}.
243
+ *
244
+ * By default the recording stays on the glasses. Pass `uploadUrl` to have the
245
+ * glasses upload the finished clip to your own endpoint.
246
+ *
247
+ * @param recordingId The id returned from `startVideoRecording`.
248
+ * @param options Optional upload destination for the finished recording.
249
+ */
250
+ async stopVideoRecording(recordingId: string, options: StopVideoRecordingOptions = {}): Promise<void> {
251
+ await this.session.sendRequest<void>({
252
+ type: MiniappRequestType.VIDEO_RECORDING_STOP,
253
+ recordingId,
254
+ uploadUrl: options.uploadUrl,
255
+ uploadAuthToken: options.uploadAuthToken,
256
+ })
257
+ }
258
+ }
@@ -0,0 +1,94 @@
1
+ /**
2
+ * @fileoverview CloudModule — local miniapp view of the phone-owned cloud client.
3
+ *
4
+ * Miniapps do not own or configure the cloud connection. The phone owns one
5
+ * cloud-client instance and pushes status snapshots here so miniapps can render
6
+ * honest UX such as online/offline captions indicators.
7
+ */
8
+
9
+ import {EventEmitter} from "eventemitter3"
10
+
11
+ import {MiniappSession} from "../session"
12
+ import type {UnsubscribeFn} from "./events"
13
+
14
+ export type CloudClientConnectionStatus = "connected" | "connecting" | "reconnecting" | "disconnected"
15
+ export type CloudClientAudioTransport = "udp" | "ws" | "offline" | "none"
16
+
17
+ export interface CloudClientStatus {
18
+ status: CloudClientConnectionStatus
19
+ audioTransport: CloudClientAudioTransport
20
+ }
21
+
22
+ type CloudEmitterEvents = {
23
+ status: (status: CloudClientStatus) => void
24
+ }
25
+
26
+ export const CLOUD_STATUS_STREAM = "_cloud_status"
27
+
28
+ const DEFAULT_STATUS: CloudClientStatus = {
29
+ status: "disconnected",
30
+ audioTransport: "none",
31
+ }
32
+
33
+ function normalizeCloudStatus(data: unknown): CloudClientStatus | null {
34
+ if (!data || typeof data !== "object") return null
35
+ const raw = data as Record<string, unknown>
36
+ const status = raw.status
37
+ const audioTransport = raw.audioTransport
38
+
39
+ if (status !== "connected" && status !== "connecting" && status !== "reconnecting" && status !== "disconnected") {
40
+ return null
41
+ }
42
+ if (
43
+ audioTransport !== "udp" &&
44
+ audioTransport !== "ws" &&
45
+ audioTransport !== "offline" &&
46
+ audioTransport !== "none"
47
+ ) {
48
+ return null
49
+ }
50
+
51
+ return {status, audioTransport}
52
+ }
53
+
54
+ export class CloudModule {
55
+ private readonly emitter = new EventEmitter<CloudEmitterEvents>()
56
+ private current: CloudClientStatus = {...DEFAULT_STATUS}
57
+
58
+ constructor(private readonly session: MiniappSession) {
59
+ this.session._subscribe(CLOUD_STATUS_STREAM, (data) => {
60
+ const next = normalizeCloudStatus(data)
61
+ if (!next) return
62
+ if (next.status === this.current.status && next.audioTransport === this.current.audioTransport) return
63
+ this.current = next
64
+ this.emitter.emit("status", {...this.current})
65
+ })
66
+ }
67
+
68
+ /** Latest status snapshot pushed by the phone runtime. */
69
+ get status(): CloudClientStatus {
70
+ return {...this.current}
71
+ }
72
+
73
+ /** True when the phone's cloud-client runtime handshake is complete. */
74
+ get connected(): boolean {
75
+ return this.current.status === "connected"
76
+ }
77
+
78
+ /** Method alias for callers who prefer function-style state reads. */
79
+ isConnected(): boolean {
80
+ return this.connected
81
+ }
82
+
83
+ /**
84
+ * Subscribe to cloud-client status updates. The current value is delivered
85
+ * immediately so UI code can hydrate without waiting for the next transition.
86
+ */
87
+ onStatusChanged(handler: (status: CloudClientStatus) => void): UnsubscribeFn {
88
+ handler({...this.current})
89
+ this.emitter.on("status", handler)
90
+ return () => {
91
+ this.emitter.off("status", handler)
92
+ }
93
+ }
94
+ }