@mentra/engine 3.2.0-dev.137 → 3.2.0-dev.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +15 -9
- package/build/engine.js.map +1 -1
- package/build/facades/ota.d.ts +1 -0
- package/build/facades/ota.d.ts.map +1 -1
- package/build/facades/ota.js +1 -0
- package/build/facades/ota.js.map +1 -1
- package/build/facades/reports.d.ts.map +1 -1
- package/build/facades/reports.js +3 -0
- package/build/facades/reports.js.map +1 -1
- package/build/generated/releaseMetadata.js +5 -5
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
- package/build/react/MentraLiveOtaFlow.js +12 -0
- package/build/react/MentraLiveOtaFlow.js.map +1 -1
- package/build/react/useMentraLiveOta.d.ts +3 -1
- package/build/react/useMentraLiveOta.d.ts.map +1 -1
- package/build/react/useMentraLiveOta.js +19 -1
- package/build/react/useMentraLiveOta.js.map +1 -1
- package/build/runtime/bootstrap.d.ts +46 -3
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js +60 -1
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/runtime/config.d.ts +4 -0
- package/build/runtime/config.d.ts.map +1 -1
- package/build/runtime/config.js.map +1 -1
- package/build/runtime/streamConfig.d.ts.map +1 -1
- package/build/runtime/streamConfig.js +6 -0
- package/build/runtime/streamConfig.js.map +1 -1
- package/build/services/AppRegistry.d.ts +15 -1
- package/build/services/AppRegistry.d.ts.map +1 -1
- package/build/services/AppRegistry.js +48 -12
- package/build/services/AppRegistry.js.map +1 -1
- package/build/services/CloudClientService.d.ts +3 -1
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +75 -18
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +54 -12
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/MiniappLauncher.d.ts.map +1 -1
- package/build/services/MiniappLauncher.js +9 -1
- package/build/services/MiniappLauncher.js.map +1 -1
- package/build/services/NavigationHandlers.d.ts.map +1 -1
- package/build/services/NavigationHandlers.js +8 -0
- package/build/services/NavigationHandlers.js.map +1 -1
- package/build/services/OtaUpdateCheckService.d.ts +10 -1
- package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
- package/build/services/OtaUpdateCheckService.js +24 -5
- package/build/services/OtaUpdateCheckService.js.map +1 -1
- package/build/services/miniappInstallIdentity.d.ts +10 -0
- package/build/services/miniappInstallIdentity.d.ts.map +1 -0
- package/build/services/miniappInstallIdentity.js +10 -0
- package/build/services/miniappInstallIdentity.js.map +1 -0
- package/build/services/otaManifestPolicy.d.ts +3 -0
- package/build/services/otaManifestPolicy.d.ts.map +1 -1
- package/build/services/otaManifestPolicy.js +8 -0
- package/build/services/otaManifestPolicy.js.map +1 -1
- package/build/services/otaManifestUrl.d.ts.map +1 -1
- package/build/services/otaManifestUrl.js +10 -2
- package/build/services/otaManifestUrl.js.map +1 -1
- package/build/stores/glasses.d.ts.map +1 -1
- package/build/stores/glasses.js +6 -0
- package/build/stores/glasses.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +20 -3
- package/build/stores/settings.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +22 -12
- package/src/facades/ota.ts +1 -0
- package/src/facades/reports.ts +4 -6
- package/src/generated/releaseMetadata.ts +5 -5
- package/src/index.ts +2 -0
- package/src/react/MentraLiveOtaFlow.tsx +21 -0
- package/src/react/useMentraLiveOta.ts +23 -2
- package/src/runtime/bootstrap.ts +117 -4
- package/src/runtime/config.ts +4 -0
- package/src/runtime/streamConfig.ts +4 -0
- package/src/services/AppRegistry.ts +94 -25
- package/src/services/CloudClientService.ts +70 -24
- package/src/services/LocalMiniappRuntime.ts +85 -26
- package/src/services/MiniappLauncher.ts +15 -1
- package/src/services/NavigationHandlers.ts +8 -0
- package/src/services/OtaUpdateCheckService.ts +32 -6
- package/src/services/miniappInstallIdentity.ts +18 -0
- package/src/services/otaManifestPolicy.ts +11 -0
- package/src/services/otaManifestUrl.ts +10 -2
- package/src/stores/glasses.ts +6 -0
- package/src/stores/settings.ts +20 -3
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* this service.
|
|
15
15
|
*/
|
|
16
16
|
import {CloudClient, setNativeHttp, setNativeUdp, setSecureStorage} from "@mentra/cloud-client/react-native"
|
|
17
|
+
import {DEFAULT_REFRESH_TOKEN_KEY} from "@mentra/cloud-client"
|
|
17
18
|
import type {PreinstalledMiniappRegistry, RuntimeSnapshot} from "@mentra/cloud-client/react-native"
|
|
18
19
|
import type {SubjectTokenType} from "@mentra/cloud-client"
|
|
19
20
|
import {Platform} from "react-native"
|
|
@@ -21,7 +22,7 @@ import type {AudioSubscription, TranscriptionData, TranslationData} from "@mentr
|
|
|
21
22
|
|
|
22
23
|
import BluetoothSdk from "@mentra/bluetooth-sdk/internal"
|
|
23
24
|
import CrustModule from "@mentra/crust"
|
|
24
|
-
import {getAuth, getConfigValues} from "../runtime/bootstrap"
|
|
25
|
+
import {getAuth, getConfigValues, isFeatureEnabled} from "../runtime/bootstrap"
|
|
25
26
|
import {useSettingsStore, SETTINGS} from "../stores/settings"
|
|
26
27
|
import {type CloudClientStatusSnapshot, type MiniappAuthToken} from "../runtime/config"
|
|
27
28
|
import {createCloudUdpSocket} from "../utils/cloudClient/RnUdpAdapter"
|
|
@@ -69,7 +70,7 @@ let persistentFailureNotified = false
|
|
|
69
70
|
let audioSubscriptions: AudioSubscription[] = []
|
|
70
71
|
let transportsReady = false
|
|
71
72
|
/** Endpoints to build with — seeded from config, overridable via reconnect(). */
|
|
72
|
-
let endpointsOverride: {core
|
|
73
|
+
let endpointsOverride: {core?: string; runtime: string} | null = null
|
|
73
74
|
let runtimeStatusUnsubscribe: (() => void) | null = null
|
|
74
75
|
let transcriptUnsubscribe: (() => void) | null = null
|
|
75
76
|
let translationUnsubscribe: (() => void) | null = null
|
|
@@ -98,12 +99,15 @@ const translationListeners = new Set<(d: TranslationData) => void>()
|
|
|
98
99
|
const statusListeners = new Set<(snapshot: CloudClientStatusSnapshot) => void>()
|
|
99
100
|
const connectionListeners = new Set<(connected: boolean) => void>()
|
|
100
101
|
|
|
101
|
-
function resolveEndpoints(): {core
|
|
102
|
+
function resolveEndpoints(): {core?: string; runtime: string} {
|
|
102
103
|
if (endpointsOverride) return endpointsOverride
|
|
103
104
|
const cfg = getConfigValues()
|
|
105
|
+
const runtime = cfg.runtimeUrl === null ? "" : cfg.runtimeUrl?.trim() || FALLBACK_RUNTIME_URL
|
|
106
|
+
if (!runtime) throw new Error("cloudClient: Runtime endpoint is not configured")
|
|
107
|
+
const core = cfg.coreUrl === null ? undefined : cfg.coreUrl?.trim() || FALLBACK_CORE_URL
|
|
104
108
|
return {
|
|
105
|
-
core:
|
|
106
|
-
runtime
|
|
109
|
+
...(core ? {core} : {}),
|
|
110
|
+
runtime,
|
|
107
111
|
}
|
|
108
112
|
}
|
|
109
113
|
|
|
@@ -159,7 +163,7 @@ function frameSizeBytes(): Lc3FrameSizeBytes {
|
|
|
159
163
|
|
|
160
164
|
async function getSubjectToken(): Promise<{token: string; type: SubjectTokenType}> {
|
|
161
165
|
const a = getAuth()
|
|
162
|
-
if (!a) throw new Error("cloudClient:
|
|
166
|
+
if (!a?.getSubjectToken) throw new Error("cloudClient: Core subject-token auth is not configured")
|
|
163
167
|
const r = await a.getSubjectToken()
|
|
164
168
|
// IslandAuth's SubjectTokenType is intentionally open (`string & {}`) so OEMs
|
|
165
169
|
// can use other token kinds; cloud-client's is a closed union. The host's
|
|
@@ -408,13 +412,23 @@ function construct(): void {
|
|
|
408
412
|
const endpoints = resolveEndpoints()
|
|
409
413
|
console.log(`${LOG_TAG}: endpoints ${JSON.stringify(endpoints)}`)
|
|
410
414
|
|
|
411
|
-
const
|
|
412
|
-
|
|
415
|
+
const configuredAuth = getAuth()
|
|
416
|
+
if (!configuredAuth) throw new Error("cloudClient: engine.configure({auth}) not called")
|
|
417
|
+
const coreAuth = configuredAuth.getSubjectToken ? {getSubjectToken} : undefined
|
|
418
|
+
const auth = configuredAuth.getRuntimeToken
|
|
413
419
|
? {
|
|
414
|
-
core: coreAuth,
|
|
415
|
-
runtime: {getToken:
|
|
420
|
+
...(coreAuth && endpoints.core ? {core: coreAuth} : {}),
|
|
421
|
+
runtime: {getToken: configuredAuth.getRuntimeToken},
|
|
416
422
|
}
|
|
417
|
-
:
|
|
423
|
+
: shouldUseLocalDevRuntimeToken(endpoints)
|
|
424
|
+
? {
|
|
425
|
+
...(coreAuth ? {core: coreAuth} : {}),
|
|
426
|
+
runtime: {getToken: (opts?: {forceRefresh?: boolean}) => getLocalDevRuntimeToken(endpoints.runtime, opts)},
|
|
427
|
+
}
|
|
428
|
+
: coreAuth && endpoints.core
|
|
429
|
+
? {core: coreAuth, runtime: {source: "core" as const}}
|
|
430
|
+
: null
|
|
431
|
+
if (!auth) throw new Error("cloudClient: no Runtime authentication mode is configured")
|
|
418
432
|
|
|
419
433
|
client = new CloudClient({
|
|
420
434
|
endpoints,
|
|
@@ -422,6 +436,7 @@ function construct(): void {
|
|
|
422
436
|
// LC3 at 16 kHz with the frame size the encoder emits.
|
|
423
437
|
audio: {codec: "lc3", sampleRate: 16000, frameSizeBytes: frameSizeBytes()},
|
|
424
438
|
auth,
|
|
439
|
+
authStorageKey: getConfigValues().cloudAuthStorageKey,
|
|
425
440
|
timers: {
|
|
426
441
|
setTimeout: (callback, delayMs) => BgTimer.setTimeout(callback, delayMs),
|
|
427
442
|
clearTimeout: (handle) => BgTimer.clearTimeout(handle as number),
|
|
@@ -470,7 +485,9 @@ function construct(): void {
|
|
|
470
485
|
connected = false
|
|
471
486
|
console.log(`${LOG_TAG}: runtime disconnected (${info.reason})`)
|
|
472
487
|
console.log(
|
|
473
|
-
`${LOG_TAG}: debug: ws-session-debug disconnected reason=${
|
|
488
|
+
`${LOG_TAG}: debug: ws-session-debug disconnected reason=${
|
|
489
|
+
info.reason
|
|
490
|
+
} willStayDown=${/superseded by newer session/i.test(info.reason)}`,
|
|
474
491
|
)
|
|
475
492
|
// Arm the persistent-failure alarm once; if we're still down when it fires, raise
|
|
476
493
|
// the notification. A reconnect within the window cancels it (onConnected above).
|
|
@@ -493,17 +510,22 @@ function construct(): void {
|
|
|
493
510
|
console.warn(`${LOG_TAG}: runtime error: ${err.code}`)
|
|
494
511
|
})
|
|
495
512
|
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
.
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
513
|
+
if (getConfigValues().runtimeRealtimeSession !== false) {
|
|
514
|
+
// Best-effort connect. Do not crash the app if the dev cloud is unreachable.
|
|
515
|
+
c.runtime
|
|
516
|
+
.connect()
|
|
517
|
+
.then(() => console.log(`${LOG_TAG}: connect() resolved`))
|
|
518
|
+
.catch((err) => console.warn(`${LOG_TAG}: connect() failed: ${err?.message ?? err}`))
|
|
519
|
+
startLc3FrameSizeWatcher()
|
|
520
|
+
} else {
|
|
521
|
+
console.log(`${LOG_TAG}: Runtime live session disabled; REST capabilities remain available`)
|
|
522
|
+
}
|
|
505
523
|
|
|
506
|
-
|
|
524
|
+
if (c.core) {
|
|
525
|
+
syncCoreAccessTokenToBluetooth().catch((err) =>
|
|
526
|
+
console.warn(`${LOG_TAG}: initial Bluetooth core_token sync failed: ${(err as Error)?.message ?? err}`),
|
|
527
|
+
)
|
|
528
|
+
}
|
|
507
529
|
}
|
|
508
530
|
|
|
509
531
|
/**
|
|
@@ -511,6 +533,14 @@ function construct(): void {
|
|
|
511
533
|
* and the runtime-hook wiring.
|
|
512
534
|
*/
|
|
513
535
|
export const cloudClientService = {
|
|
536
|
+
async clearAuthSession(): Promise<void> {
|
|
537
|
+
if (client) {
|
|
538
|
+
await client.auth.clearSession()
|
|
539
|
+
return
|
|
540
|
+
}
|
|
541
|
+
await cloudSecureStore.delete(getConfigValues().cloudAuthStorageKey ?? DEFAULT_REFRESH_TOKEN_KEY)
|
|
542
|
+
},
|
|
543
|
+
|
|
514
544
|
/**
|
|
515
545
|
* Construct (once) + connect the client. Idempotent. Best-effort connect — a
|
|
516
546
|
* failure is logged and the app keeps running. Requires
|
|
@@ -527,7 +557,7 @@ export const cloudClientService = {
|
|
|
527
557
|
* a prior override and fall back to the boot config (so cleared/default cloud
|
|
528
558
|
* URLs don't keep reconnecting to a stale override); omit to keep the current.
|
|
529
559
|
*/
|
|
530
|
-
reconnect(endpoints?: {core
|
|
560
|
+
reconnect(endpoints?: {core?: string; runtime: string} | null): void {
|
|
531
561
|
if (endpoints !== undefined) endpointsOverride = endpoints
|
|
532
562
|
try {
|
|
533
563
|
client?.runtime.close()
|
|
@@ -565,6 +595,8 @@ export const cloudClientService = {
|
|
|
565
595
|
*/
|
|
566
596
|
async resolveMentraUserId(): Promise<string> {
|
|
567
597
|
return localMiniappUserIdentity.resolve(async () => {
|
|
598
|
+
const hostIdentity = getAuth()?.getUserId
|
|
599
|
+
if (hostIdentity) return await hostIdentity()
|
|
568
600
|
if (!client) this.init()
|
|
569
601
|
const c = client
|
|
570
602
|
if (!c) throw new Error("cloud client not initialized")
|
|
@@ -636,14 +668,17 @@ export const cloudClientService = {
|
|
|
636
668
|
|
|
637
669
|
/** Managed stream (cloud-v2): provision ingest+playback on the runtime. */
|
|
638
670
|
startManagedStream(opts: Record<string, unknown> = {}) {
|
|
671
|
+
if (!isFeatureEnabled("managedStreams")) throw new Error("managed streams are disabled by this deployment")
|
|
639
672
|
if (!client) throw new Error("cloud client not connected")
|
|
640
673
|
return client.runtime.startManagedStream(opts)
|
|
641
674
|
},
|
|
642
675
|
getManagedStreamStatus(streamId: string) {
|
|
676
|
+
if (!isFeatureEnabled("managedStreams")) throw new Error("managed streams are disabled by this deployment")
|
|
643
677
|
if (!client) throw new Error("cloud client not connected")
|
|
644
678
|
return client.runtime.getManagedStreamStatus(streamId)
|
|
645
679
|
},
|
|
646
680
|
stopManagedStream(streamId: string) {
|
|
681
|
+
if (!isFeatureEnabled("managedStreams")) throw new Error("managed streams are disabled by this deployment")
|
|
647
682
|
if (!client) throw new Error("cloud client not connected")
|
|
648
683
|
return client.runtime.stopManagedStream(streamId)
|
|
649
684
|
},
|
|
@@ -695,7 +730,13 @@ export const cloudClientService = {
|
|
|
695
730
|
},
|
|
696
731
|
|
|
697
732
|
getCoreUrl(): string {
|
|
698
|
-
|
|
733
|
+
const core = resolveEndpoints().core
|
|
734
|
+
if (!core) throw new Error("cloud client core is unavailable")
|
|
735
|
+
return core
|
|
736
|
+
},
|
|
737
|
+
|
|
738
|
+
hasCore(): boolean {
|
|
739
|
+
return Boolean(client?.core)
|
|
699
740
|
},
|
|
700
741
|
|
|
701
742
|
syncCoreTokenToBluetooth(): Promise<string> {
|
|
@@ -711,6 +752,7 @@ export const cloudClientService = {
|
|
|
711
752
|
|
|
712
753
|
tts: {
|
|
713
754
|
speak(text: string, options?: Parameters<CloudClient["runtime"]["tts"]["speak"]>[1]) {
|
|
755
|
+
if (!isFeatureEnabled("cloudSpeech")) throw new Error("cloud speech is disabled by this deployment")
|
|
714
756
|
if (!client) throw new Error("cloud client not connected")
|
|
715
757
|
return client.runtime.tts.speak(text, options)
|
|
716
758
|
},
|
|
@@ -718,18 +760,22 @@ export const cloudClientService = {
|
|
|
718
760
|
|
|
719
761
|
maps: {
|
|
720
762
|
directions(req: Parameters<CloudClient["runtime"]["maps"]["directions"]>[0]) {
|
|
763
|
+
if (!isFeatureEnabled("navigation")) throw new Error("navigation is disabled by this deployment")
|
|
721
764
|
if (!client) throw new Error("cloud client not connected")
|
|
722
765
|
return client.runtime.maps.directions(req)
|
|
723
766
|
},
|
|
724
767
|
reverseGeocode(coord: Parameters<CloudClient["runtime"]["maps"]["reverseGeocode"]>[0]) {
|
|
768
|
+
if (!isFeatureEnabled("navigation")) throw new Error("navigation is disabled by this deployment")
|
|
725
769
|
if (!client) throw new Error("cloud client not connected")
|
|
726
770
|
return client.runtime.maps.reverseGeocode(coord)
|
|
727
771
|
},
|
|
728
772
|
placeAutocomplete(req: Parameters<CloudClient["runtime"]["maps"]["placeAutocomplete"]>[0]) {
|
|
773
|
+
if (!isFeatureEnabled("navigation")) throw new Error("navigation is disabled by this deployment")
|
|
729
774
|
if (!client) throw new Error("cloud client not connected")
|
|
730
775
|
return client.runtime.maps.placeAutocomplete(req)
|
|
731
776
|
},
|
|
732
777
|
placeDetails(req: Parameters<CloudClient["runtime"]["maps"]["placeDetails"]>[0]) {
|
|
778
|
+
if (!isFeatureEnabled("navigation")) throw new Error("navigation is disabled by this deployment")
|
|
733
779
|
if (!client) throw new Error("cloud client not connected")
|
|
734
780
|
return client.runtime.maps.placeDetails(req)
|
|
735
781
|
},
|
|
@@ -66,7 +66,7 @@ import {
|
|
|
66
66
|
type MiniappAuthToken,
|
|
67
67
|
type TtsSynthesisResult,
|
|
68
68
|
} from "../runtime/config"
|
|
69
|
-
import {getAnalytics, getUiSeams} from "../runtime/bootstrap"
|
|
69
|
+
import {getAnalytics, getMiniappConfiguration, getUiSeams, isFeatureEnabled} from "../runtime/bootstrap"
|
|
70
70
|
import {invokeScanQrSeam} from "../runtime/scanQrSeam"
|
|
71
71
|
import {normalizeStreamAudioConfig, normalizeStreamVideoConfig, resolveCaptureAudio} from "../runtime/streamConfig"
|
|
72
72
|
import {toLanguageHint} from "@mentra/cloud-protocol/languages"
|
|
@@ -76,7 +76,7 @@ import ttsModelManager from "./TTSModelManager"
|
|
|
76
76
|
import {NavigationHandlers} from "./NavigationHandlers"
|
|
77
77
|
import type {ClientApp} from "../types/applet"
|
|
78
78
|
import {useAppStatusStore} from "../stores/apps"
|
|
79
|
-
import {getDevAppAttestation, getDevAppSourcePackage} from "./AppRegistry"
|
|
79
|
+
import appRegistry, {getDevAppAttestation, getDevAppSourcePackage} from "./AppRegistry"
|
|
80
80
|
import {resolveForegroundLocationPermission} from "./ForegroundLocationPermission"
|
|
81
81
|
import {advanceMiniappPingLiveness} from "./MiniappLiveness"
|
|
82
82
|
import {listPhoneCalendarEvents, PhoneCalendarError} from "./PhoneCalendarService"
|
|
@@ -189,14 +189,7 @@ const LOG_TAG = "LOCAL_MINIAPP"
|
|
|
189
189
|
const DIAGNOSTIC_MAX_LIST_ITEMS = 100
|
|
190
190
|
const DIAGNOSTIC_MAX_STRING_LENGTH = 512
|
|
191
191
|
|
|
192
|
-
|
|
193
|
-
return [...values]
|
|
194
|
-
.map((value) => value.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH))
|
|
195
|
-
.sort()
|
|
196
|
-
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
192
|
+
const SYSTEM_MINIAPP_PACKAGE_SET = new Set([
|
|
200
193
|
"com.mentra.camera",
|
|
201
194
|
"com.mentra.gallery",
|
|
202
195
|
"com.mentra.settings",
|
|
@@ -207,6 +200,14 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
|
|
|
207
200
|
"com.mentra.feedback",
|
|
208
201
|
"com.mentra.miniappdev",
|
|
209
202
|
])
|
|
203
|
+
|
|
204
|
+
function diagnosticStringList(values: Iterable<string>): string[] {
|
|
205
|
+
return [...values]
|
|
206
|
+
.map((value) => value.slice(0, DIAGNOSTIC_MAX_STRING_LENGTH))
|
|
207
|
+
.sort()
|
|
208
|
+
.slice(0, DIAGNOSTIC_MAX_LIST_ITEMS)
|
|
209
|
+
}
|
|
210
|
+
|
|
210
211
|
const PING_INTERVAL_MS = 5_000
|
|
211
212
|
const MINIAPP_AUTH_REFRESH_HEADROOM_MS = 5 * 60 * 1000
|
|
212
213
|
const MINIAPP_AUTH_REFRESH_MIN_DELAY_MS = 5_000
|
|
@@ -1445,6 +1446,7 @@ class LocalMiniappRuntime {
|
|
|
1445
1446
|
packageName,
|
|
1446
1447
|
capabilities,
|
|
1447
1448
|
permissions: declaredPermissions,
|
|
1449
|
+
configuration: getMiniappConfiguration(packageName),
|
|
1448
1450
|
hostFeatures: {captureAudio: true},
|
|
1449
1451
|
...(initialAuth ? {auth: initialAuth} : {}),
|
|
1450
1452
|
},
|
|
@@ -1468,6 +1470,9 @@ class LocalMiniappRuntime {
|
|
|
1468
1470
|
}
|
|
1469
1471
|
|
|
1470
1472
|
private async requestMiniappAuth(packageName: string, opts?: {minTtlMs?: number}): Promise<MiniappAuthToken | null> {
|
|
1473
|
+
// Core owns miniapp-backend token minting. A Runtime-only deployment has
|
|
1474
|
+
// no such capability, so fail once instead of entering the retry loop.
|
|
1475
|
+
if (!cloudClientService.hasCore()) return null
|
|
1471
1476
|
const authPackageName = getDevAppSourcePackage(packageName) ?? packageName
|
|
1472
1477
|
const devAttestation = getDevAppAttestation(packageName) ?? undefined
|
|
1473
1478
|
return cloudClientService.getMiniappAuthToken(authPackageName, {
|
|
@@ -1705,6 +1710,34 @@ class LocalMiniappRuntime {
|
|
|
1705
1710
|
return
|
|
1706
1711
|
}
|
|
1707
1712
|
|
|
1713
|
+
for (const subscription of rawStreams ?? []) {
|
|
1714
|
+
const stream = typeof subscription === "string" ? subscription : subscription.stream
|
|
1715
|
+
if (stream.startsWith("translation:") && !isFeatureEnabled("cloudSpeech")) {
|
|
1716
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1717
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
1718
|
+
message: "Cloud speech is disabled by this deployment",
|
|
1719
|
+
})
|
|
1720
|
+
return
|
|
1721
|
+
}
|
|
1722
|
+
if (!stream.startsWith("transcription:")) continue
|
|
1723
|
+
const forceLocal = typeof subscription === "object" && subscription.forceLocal === true
|
|
1724
|
+
const includeCloud = typeof subscription === "object" && subscription.includeCloud === true
|
|
1725
|
+
if (forceLocal && !isFeatureEnabled("onDeviceSpeech")) {
|
|
1726
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1727
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
1728
|
+
message: "On-device speech is disabled by this deployment",
|
|
1729
|
+
})
|
|
1730
|
+
return
|
|
1731
|
+
}
|
|
1732
|
+
if ((!forceLocal || includeCloud) && !isFeatureEnabled("cloudSpeech")) {
|
|
1733
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
1734
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
1735
|
+
message: "Cloud speech is disabled by this deployment",
|
|
1736
|
+
})
|
|
1737
|
+
return
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1708
1741
|
console.log(`${LOG_TAG}: SUBSCRIBE from ${packageName}: [${streams.join(", ")}]`)
|
|
1709
1742
|
|
|
1710
1743
|
// Gate each stream on the permission type its data requires. The manifest
|
|
@@ -2244,11 +2277,11 @@ class LocalMiniappRuntime {
|
|
|
2244
2277
|
typeof rawText === "string"
|
|
2245
2278
|
? [rawText.trim()].filter(Boolean)
|
|
2246
2279
|
: Array.isArray(rawText)
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2280
|
+
? rawText
|
|
2281
|
+
.filter((sentence): sentence is string => typeof sentence === "string")
|
|
2282
|
+
.map((sentence) => sentence.trim())
|
|
2283
|
+
.filter(Boolean)
|
|
2284
|
+
: []
|
|
2252
2285
|
const enableSanitization = payload.enableSanitization !== false
|
|
2253
2286
|
const sentences = prepareTtsSentences(rawSentences, enableSanitization)
|
|
2254
2287
|
if (sentences.length === 0) {
|
|
@@ -2333,6 +2366,7 @@ class LocalMiniappRuntime {
|
|
|
2333
2366
|
|
|
2334
2367
|
const playOfflineTts = async (reason?: string): Promise<boolean> => {
|
|
2335
2368
|
if (run.cancelled) return true
|
|
2369
|
+
if (!isFeatureEnabled("onDeviceSpeech")) return false
|
|
2336
2370
|
if (!offlineSupportsVoice || !(await ttsModelManager.isModelAvailable())) {
|
|
2337
2371
|
return false
|
|
2338
2372
|
}
|
|
@@ -2577,7 +2611,9 @@ class LocalMiniappRuntime {
|
|
|
2577
2611
|
const permission = await resolveForegroundLocationPermission(Location, () => AppState.currentState)
|
|
2578
2612
|
const {status} = permission
|
|
2579
2613
|
console.log(
|
|
2580
|
-
`${LOG_TAG}: location poll permission status=${status} appState=${AppState.currentState} elapsed=${
|
|
2614
|
+
`${LOG_TAG}: location poll permission status=${status} appState=${AppState.currentState} elapsed=${
|
|
2615
|
+
Date.now() - permissionStartedAt
|
|
2616
|
+
}ms`,
|
|
2581
2617
|
)
|
|
2582
2618
|
if (status !== "granted") {
|
|
2583
2619
|
this.sendResult(packageName, requestId, false, undefined, {
|
|
@@ -3138,11 +3174,7 @@ class LocalMiniappRuntime {
|
|
|
3138
3174
|
* session.system.scanQr — host camera overlay. Must not clear miniapp
|
|
3139
3175
|
* foreground; the host seam is responsible for presenting a Modal on top.
|
|
3140
3176
|
*/
|
|
3141
|
-
private async handleScanQr(
|
|
3142
|
-
packageName: string,
|
|
3143
|
-
payload: Record<string, unknown>,
|
|
3144
|
-
requestId?: string,
|
|
3145
|
-
): Promise<void> {
|
|
3177
|
+
private async handleScanQr(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
|
|
3146
3178
|
try {
|
|
3147
3179
|
const result = await invokeScanQrSeam(getUiSeams().scanQr, payload)
|
|
3148
3180
|
this.sendResult(packageName, requestId, true, result)
|
|
@@ -3465,7 +3497,15 @@ class LocalMiniappRuntime {
|
|
|
3465
3497
|
return
|
|
3466
3498
|
}
|
|
3467
3499
|
// A stream is the glasses camera (and mic) leaving the device; gate it like a photo.
|
|
3468
|
-
if (
|
|
3500
|
+
if (
|
|
3501
|
+
!this.requireManifestPermission(
|
|
3502
|
+
packageName,
|
|
3503
|
+
requestId,
|
|
3504
|
+
"CAMERA",
|
|
3505
|
+
"to stream the camera",
|
|
3506
|
+
MiniappRequestType.STREAM_START,
|
|
3507
|
+
)
|
|
3508
|
+
) {
|
|
3469
3509
|
return
|
|
3470
3510
|
}
|
|
3471
3511
|
try {
|
|
@@ -3582,8 +3622,20 @@ class LocalMiniappRuntime {
|
|
|
3582
3622
|
// A meeting puts the glasses camera and mic in front of remote strangers; it
|
|
3583
3623
|
// needs both declared, same as photo/stream and mic capture do individually.
|
|
3584
3624
|
if (
|
|
3585
|
-
!this.requireManifestPermission(
|
|
3586
|
-
|
|
3625
|
+
!this.requireManifestPermission(
|
|
3626
|
+
packageName,
|
|
3627
|
+
requestId,
|
|
3628
|
+
"CAMERA",
|
|
3629
|
+
"to join a meeting",
|
|
3630
|
+
MiniappRequestType.MEETING_JOIN,
|
|
3631
|
+
) ||
|
|
3632
|
+
!this.requireManifestPermission(
|
|
3633
|
+
packageName,
|
|
3634
|
+
requestId,
|
|
3635
|
+
"MICROPHONE",
|
|
3636
|
+
"to join a meeting",
|
|
3637
|
+
MiniappRequestType.MEETING_JOIN,
|
|
3638
|
+
)
|
|
3587
3639
|
) {
|
|
3588
3640
|
return
|
|
3589
3641
|
}
|
|
@@ -4333,8 +4385,15 @@ class LocalMiniappRuntime {
|
|
|
4333
4385
|
}
|
|
4334
4386
|
|
|
4335
4387
|
private isSystemPackage(packageName: string): boolean {
|
|
4336
|
-
|
|
4337
|
-
|
|
4388
|
+
const app = this.interopApps().find((candidate) => candidate.packageName === packageName)
|
|
4389
|
+
if (SYSTEM_MINIAPP_PACKAGE_SET.has(packageName)) {
|
|
4390
|
+
if (app?.offline) return true
|
|
4391
|
+
if (app?.local && app.version) {
|
|
4392
|
+
return appRegistry.getReleaseIdentity(packageName, app.version)?.source === "bundled_asset"
|
|
4393
|
+
}
|
|
4394
|
+
return false
|
|
4395
|
+
}
|
|
4396
|
+
return app?.isMiniappDev === true
|
|
4338
4397
|
}
|
|
4339
4398
|
|
|
4340
4399
|
private async startInteropApp(packageName: string): Promise<boolean> {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
|
|
26
26
|
import {File} from "expo-file-system"
|
|
27
27
|
|
|
28
|
+
import {isInstalledMiniappAllowed, isLocalMiniappPackageAllowed} from "../runtime/bootstrap"
|
|
28
29
|
import {resolveDevBundleSource} from "../utils/devMiniappSnapshot"
|
|
29
30
|
import {storage} from "../utils/storage/storage"
|
|
30
31
|
import appRegistry, {getLocalAppRunningState, saveLocalAppRunningState} from "./AppRegistry"
|
|
@@ -134,7 +135,7 @@ class MiniappLauncher {
|
|
|
134
135
|
if (!entry?.background) return null
|
|
135
136
|
|
|
136
137
|
// Use the host that actually answered — may differ from the stored IP
|
|
137
|
-
// after a laptop Wi-Fi change (mDNS / Metro failover inside
|
|
138
|
+
// after a laptop Wi-Fi change (mDNS / Metro failover inside resolveDevBundleSource).
|
|
138
139
|
const base = resolvedUrl.replace(/\/$/, "")
|
|
139
140
|
// entry.* are bundle-root paths (dist/ stripped); the dev server serves
|
|
140
141
|
// files relative to cwd, so prepend dist/.
|
|
@@ -243,6 +244,9 @@ class MiniappLauncher {
|
|
|
243
244
|
* {@link LocalMiniappRuntime.waitForConnect}.
|
|
244
245
|
*/
|
|
245
246
|
async ensureRunning(packageName: string, hints?: LaunchHints): Promise<LaunchResult> {
|
|
247
|
+
if (!isLocalMiniappPackageAllowed(packageName)) {
|
|
248
|
+
throw new Error(`MiniappLauncher: ${packageName} is disabled by deployment policy`)
|
|
249
|
+
}
|
|
246
250
|
const router = this.requireRouter()
|
|
247
251
|
|
|
248
252
|
// Already spawned: best-effort resolve for the UI entry, never throw —
|
|
@@ -273,6 +277,16 @@ class MiniappLauncher {
|
|
|
273
277
|
if (!resolved) {
|
|
274
278
|
throw new Error(`MiniappLauncher: cannot resolve bundle for ${packageName}`)
|
|
275
279
|
}
|
|
280
|
+
const version = resolved.installedManifest?.version
|
|
281
|
+
if (
|
|
282
|
+
!isInstalledMiniappAllowed(
|
|
283
|
+
packageName,
|
|
284
|
+
version,
|
|
285
|
+
version ? appRegistry.getReleaseIdentity(packageName, version) : null,
|
|
286
|
+
)
|
|
287
|
+
) {
|
|
288
|
+
throw new Error(`MiniappLauncher: ${packageName} bundle is not authorized by deployment policy`)
|
|
289
|
+
}
|
|
276
290
|
|
|
277
291
|
// Re-check after the async resolve: a different path may have spawned it
|
|
278
292
|
// while we were fetching/reading the bundle.
|
|
@@ -26,6 +26,7 @@ import {type NavLocation, type NavRoute, type NavUpdate} from "../runtime/config
|
|
|
26
26
|
import {cloudClientService} from "./CloudClientService"
|
|
27
27
|
import navigationService from "./NavigationService"
|
|
28
28
|
import {MiniappErrorCode, MiniappResponseType, MiniappStreamType} from "@mentra/miniapp"
|
|
29
|
+
import {isFeatureEnabled} from "../runtime/bootstrap"
|
|
29
30
|
|
|
30
31
|
const LOG_TAG = "LocalMiniappRuntime"
|
|
31
32
|
|
|
@@ -81,6 +82,13 @@ export class NavigationHandlers {
|
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
async handleStart(packageName: string, payload: Record<string, unknown>, requestId?: string): Promise<void> {
|
|
85
|
+
if (!isFeatureEnabled("navigation")) {
|
|
86
|
+
this.sendResult(packageName, requestId, false, undefined, {
|
|
87
|
+
code: MiniappErrorCode.NOT_IMPLEMENTED,
|
|
88
|
+
message: "Navigation is disabled by this deployment",
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
84
92
|
console.log(`${LOG_TAG}: handleNavigationStart from ${packageName}`, JSON.stringify(payload))
|
|
85
93
|
|
|
86
94
|
// v2 wire shape sends `stops`; v1 shape sends bare lat/lng. Accept both.
|
|
@@ -78,7 +78,15 @@ export interface OtaCheckResult {
|
|
|
78
78
|
checkFailureReason?: "network" | "pin_unavailable"
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
|
|
81
|
+
/**
|
|
82
|
+
* Package the stock glasses client installs as, and the key every apps-shaped manifest pins the
|
|
83
|
+
* ASG APK under. Android forces a distinct package (`.thirdparty` suffix) on any build not signed
|
|
84
|
+
* with Mentra's release key, so a client reporting anything else is a sideloaded build that
|
|
85
|
+
* coexists with the stock system app.
|
|
86
|
+
*/
|
|
87
|
+
export const STOCK_ASG_PACKAGE = "com.mentra.asg_client"
|
|
88
|
+
|
|
89
|
+
export type OtaCheckSkippedReason = "disconnected" | "missing_build" | "dev_build" | "unofficial_client"
|
|
82
90
|
|
|
83
91
|
export interface OtaCheckCurrentGlassesResult extends OtaCheckResult {
|
|
84
92
|
updateInfo: OtaUpdateInfo | null
|
|
@@ -86,6 +94,8 @@ export interface OtaCheckCurrentGlassesResult extends OtaCheckResult {
|
|
|
86
94
|
skippedReason?: OtaCheckSkippedReason
|
|
87
95
|
manifestUrl?: string
|
|
88
96
|
buildNumber?: string
|
|
97
|
+
/** Glasses client package, when it reported one. Set on an `unofficial_client` skip. */
|
|
98
|
+
packageName?: string
|
|
89
99
|
mtkFirmwareVersion?: string
|
|
90
100
|
besFirmwareVersion?: string
|
|
91
101
|
}
|
|
@@ -100,7 +110,10 @@ export interface OtaCheckCurrentGlassesOptions {
|
|
|
100
110
|
floorVersionCode?: number
|
|
101
111
|
}
|
|
102
112
|
|
|
103
|
-
function emptyCheckResult(
|
|
113
|
+
function emptyCheckResult(
|
|
114
|
+
skippedReason?: OtaCheckSkippedReason,
|
|
115
|
+
extra?: Partial<OtaCheckCurrentGlassesResult>,
|
|
116
|
+
): OtaCheckCurrentGlassesResult {
|
|
104
117
|
return {
|
|
105
118
|
hasCheckCompleted: false,
|
|
106
119
|
updateAvailable: false,
|
|
@@ -114,6 +127,7 @@ function emptyCheckResult(skippedReason?: OtaCheckSkippedReason): OtaCheckCurren
|
|
|
114
127
|
updateInfo: null,
|
|
115
128
|
isRequired: true,
|
|
116
129
|
skippedReason,
|
|
130
|
+
...extra,
|
|
117
131
|
}
|
|
118
132
|
}
|
|
119
133
|
|
|
@@ -183,7 +197,7 @@ export function getApkUpdateDirection(
|
|
|
183
197
|
let serverVersion: number | undefined
|
|
184
198
|
let exactPin = false
|
|
185
199
|
|
|
186
|
-
const appEntry = versionJson.apps?.[
|
|
200
|
+
const appEntry = versionJson.apps?.[STOCK_ASG_PACKAGE]
|
|
187
201
|
if (appEntry) {
|
|
188
202
|
serverVersion = appEntry.versionCode
|
|
189
203
|
exactPin = true
|
|
@@ -212,8 +226,8 @@ export function getLatestVersionInfo(versionJson: VersionJson | null): VersionIn
|
|
|
212
226
|
return null
|
|
213
227
|
}
|
|
214
228
|
|
|
215
|
-
if (versionJson.apps?.[
|
|
216
|
-
return versionJson.apps[
|
|
229
|
+
if (versionJson.apps?.[STOCK_ASG_PACKAGE]) {
|
|
230
|
+
return versionJson.apps[STOCK_ASG_PACKAGE]
|
|
217
231
|
}
|
|
218
232
|
|
|
219
233
|
if (versionJson.versionCode) {
|
|
@@ -315,7 +329,7 @@ export async function checkForOtaUpdate(
|
|
|
315
329
|
// than silently completing as "up to date" — an unverifiable state must never
|
|
316
330
|
// present as a verified one.
|
|
317
331
|
const currentVersionNumber = parseInt(currentBuildNumber, 10)
|
|
318
|
-
const pinnedEntry = versionJson?.apps?.[
|
|
332
|
+
const pinnedEntry = versionJson?.apps?.[STOCK_ASG_PACKAGE]
|
|
319
333
|
const pinnedVersion = pinnedEntry?.versionCode ?? versionJson?.versionCode
|
|
320
334
|
if (
|
|
321
335
|
versionJson &&
|
|
@@ -442,6 +456,18 @@ export async function checkCurrentGlassesForUpdate(
|
|
|
442
456
|
return emptyCheckResult("missing_build")
|
|
443
457
|
}
|
|
444
458
|
|
|
459
|
+
// A sideloaded ASG client installs under its own package (Android forces the `.thirdparty`
|
|
460
|
+
// suffix on any build not signed with Mentra's release key) and coexists with the stock system
|
|
461
|
+
// app. Its build number is therefore not comparable to the manifest's pin, and installing the
|
|
462
|
+
// manifest APK would replace the stock package while the sideloaded client keeps holding the
|
|
463
|
+
// link and reporting its own unchanged version — an update prompt that can never be satisfied.
|
|
464
|
+
// Absent means the glasses predate the field: assume stock, preserving existing behavior.
|
|
465
|
+
const packageName = useGlassesStore.getState().packageName
|
|
466
|
+
if (packageName && packageName !== STOCK_ASG_PACKAGE) {
|
|
467
|
+
console.log(`OTA: check skipped - glasses run an unofficial client (${packageName})`)
|
|
468
|
+
return emptyCheckResult("unofficial_client", {buildNumber, packageName})
|
|
469
|
+
}
|
|
470
|
+
|
|
445
471
|
if (!glassesConnectedNow()) {
|
|
446
472
|
return emptyCheckResult("disconnected")
|
|
447
473
|
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface MiniappInstallExpectations {
|
|
2
|
+
packageName?: string
|
|
3
|
+
version?: string
|
|
4
|
+
rejectExistingVersion?: boolean
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function miniappInstallIdentityError(
|
|
8
|
+
manifest: {packageName: string; version: string},
|
|
9
|
+
expected?: MiniappInstallExpectations,
|
|
10
|
+
): string | null {
|
|
11
|
+
if (expected?.packageName && manifest.packageName !== expected.packageName) {
|
|
12
|
+
return `Bundle package mismatch: expected ${expected.packageName}, got ${manifest.packageName}`
|
|
13
|
+
}
|
|
14
|
+
if (expected?.version && manifest.version !== expected.version) {
|
|
15
|
+
return `Bundle version mismatch: expected ${expected.version}, got ${manifest.version}`
|
|
16
|
+
}
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
@@ -40,3 +40,14 @@ export function resolveOtaManifestPolicy({
|
|
|
40
40
|
}
|
|
41
41
|
return selectModernOtaManifestPin({developerOverride, hostReleasePin, engineReleasePin})
|
|
42
42
|
}
|
|
43
|
+
|
|
44
|
+
export function resolveDeploymentAwareOtaManifestPolicy({
|
|
45
|
+
hostPolicyConfigured,
|
|
46
|
+
...policy
|
|
47
|
+
}: Parameters<typeof resolveOtaManifestPolicy>[0] & {hostPolicyConfigured: boolean}): string | null {
|
|
48
|
+
// A selected deployment is authoritative for every glasses generation. An
|
|
49
|
+
// explicit null is a deliberate OTA-off policy, not permission to inherit a
|
|
50
|
+
// device-reported URL or Mentra's legacy public default.
|
|
51
|
+
if (hostPolicyConfigured) return trimmed(policy.hostReleasePin)
|
|
52
|
+
return resolveOtaManifestPolicy(policy)
|
|
53
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {ENGINE_RELEASE_METADATA} from "../generated/releaseMetadata"
|
|
2
2
|
import {SETTINGS, useSettingsStore} from "../stores/settings"
|
|
3
|
-
import {
|
|
3
|
+
import {resolveDeploymentAwareOtaManifestPolicy, selectModernOtaManifestPin} from "./otaManifestPolicy"
|
|
4
|
+
import {getConfigValues} from "../runtime/bootstrap"
|
|
4
5
|
|
|
5
6
|
function getOtaVersionUrlDevOverride(): string | null {
|
|
6
7
|
// Super mode only: a wrong OTA manifest can brick glasses, so a saved
|
|
@@ -14,10 +15,16 @@ function getOtaVersionUrlDevOverride(): string | null {
|
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
function getHostReleasePin(): string | null {
|
|
18
|
+
const configured = getConfigValues().otaManifestUrl
|
|
19
|
+
if (configured !== undefined) return configured?.trim() || null
|
|
17
20
|
const value = process.env.EXPO_PUBLIC_ASG_OTA_VERSION_URL?.trim()
|
|
18
21
|
return value || null
|
|
19
22
|
}
|
|
20
23
|
|
|
24
|
+
function hasDeploymentOtaPolicy(): boolean {
|
|
25
|
+
return getConfigValues().otaManifestUrl !== undefined
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
function getEmbeddedEngineReleasePin(): string | null {
|
|
22
29
|
const value = ENGINE_RELEASE_METADATA.otaManifestUrl?.trim()
|
|
23
30
|
return value || null
|
|
@@ -34,7 +41,8 @@ export function hasConfiguredModernOtaManifestPin(): boolean {
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export function resolveOtaManifestUrl(glassesUrl?: string | null, glassesBuildNumber?: string | null): string | null {
|
|
37
|
-
return
|
|
44
|
+
return resolveDeploymentAwareOtaManifestPolicy({
|
|
45
|
+
hostPolicyConfigured: hasDeploymentOtaPolicy(),
|
|
38
46
|
glassesUrl,
|
|
39
47
|
glassesBuildNumber,
|
|
40
48
|
developerOverride: getOtaVersionUrlDevOverride(),
|