@mentra/engine 3.2.0-dev.137 → 3.2.0-dev.154

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 (92) hide show
  1. package/build/engine.d.ts.map +1 -1
  2. package/build/engine.js +15 -9
  3. package/build/engine.js.map +1 -1
  4. package/build/facades/ota.d.ts +1 -0
  5. package/build/facades/ota.d.ts.map +1 -1
  6. package/build/facades/ota.js +1 -0
  7. package/build/facades/ota.js.map +1 -1
  8. package/build/facades/reports.d.ts.map +1 -1
  9. package/build/facades/reports.js +3 -0
  10. package/build/facades/reports.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +5 -5
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/index.d.ts +1 -1
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js.map +1 -1
  16. package/build/react/MentraLiveOtaFlow.d.ts.map +1 -1
  17. package/build/react/MentraLiveOtaFlow.js +12 -0
  18. package/build/react/MentraLiveOtaFlow.js.map +1 -1
  19. package/build/react/useMentraLiveOta.d.ts +3 -1
  20. package/build/react/useMentraLiveOta.d.ts.map +1 -1
  21. package/build/react/useMentraLiveOta.js +19 -1
  22. package/build/react/useMentraLiveOta.js.map +1 -1
  23. package/build/runtime/bootstrap.d.ts +46 -3
  24. package/build/runtime/bootstrap.d.ts.map +1 -1
  25. package/build/runtime/bootstrap.js +60 -1
  26. package/build/runtime/bootstrap.js.map +1 -1
  27. package/build/runtime/config.d.ts +4 -0
  28. package/build/runtime/config.d.ts.map +1 -1
  29. package/build/runtime/config.js.map +1 -1
  30. package/build/runtime/streamConfig.d.ts.map +1 -1
  31. package/build/runtime/streamConfig.js +6 -0
  32. package/build/runtime/streamConfig.js.map +1 -1
  33. package/build/services/AppRegistry.d.ts +15 -1
  34. package/build/services/AppRegistry.d.ts.map +1 -1
  35. package/build/services/AppRegistry.js +48 -12
  36. package/build/services/AppRegistry.js.map +1 -1
  37. package/build/services/CloudClientService.d.ts +3 -1
  38. package/build/services/CloudClientService.d.ts.map +1 -1
  39. package/build/services/CloudClientService.js +75 -18
  40. package/build/services/CloudClientService.js.map +1 -1
  41. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  42. package/build/services/LocalMiniappRuntime.js +54 -12
  43. package/build/services/LocalMiniappRuntime.js.map +1 -1
  44. package/build/services/MiniappLauncher.d.ts.map +1 -1
  45. package/build/services/MiniappLauncher.js +9 -1
  46. package/build/services/MiniappLauncher.js.map +1 -1
  47. package/build/services/NavigationHandlers.d.ts.map +1 -1
  48. package/build/services/NavigationHandlers.js +8 -0
  49. package/build/services/NavigationHandlers.js.map +1 -1
  50. package/build/services/OtaUpdateCheckService.d.ts +10 -1
  51. package/build/services/OtaUpdateCheckService.d.ts.map +1 -1
  52. package/build/services/OtaUpdateCheckService.js +24 -5
  53. package/build/services/OtaUpdateCheckService.js.map +1 -1
  54. package/build/services/miniappInstallIdentity.d.ts +10 -0
  55. package/build/services/miniappInstallIdentity.d.ts.map +1 -0
  56. package/build/services/miniappInstallIdentity.js +10 -0
  57. package/build/services/miniappInstallIdentity.js.map +1 -0
  58. package/build/services/otaManifestPolicy.d.ts +3 -0
  59. package/build/services/otaManifestPolicy.d.ts.map +1 -1
  60. package/build/services/otaManifestPolicy.js +8 -0
  61. package/build/services/otaManifestPolicy.js.map +1 -1
  62. package/build/services/otaManifestUrl.d.ts.map +1 -1
  63. package/build/services/otaManifestUrl.js +10 -2
  64. package/build/services/otaManifestUrl.js.map +1 -1
  65. package/build/stores/glasses.d.ts.map +1 -1
  66. package/build/stores/glasses.js +6 -0
  67. package/build/stores/glasses.js.map +1 -1
  68. package/build/stores/settings.d.ts.map +1 -1
  69. package/build/stores/settings.js +20 -3
  70. package/build/stores/settings.js.map +1 -1
  71. package/package.json +7 -7
  72. package/src/engine.ts +22 -12
  73. package/src/facades/ota.ts +1 -0
  74. package/src/facades/reports.ts +4 -6
  75. package/src/generated/releaseMetadata.ts +5 -5
  76. package/src/index.ts +2 -0
  77. package/src/react/MentraLiveOtaFlow.tsx +21 -0
  78. package/src/react/useMentraLiveOta.ts +23 -2
  79. package/src/runtime/bootstrap.ts +117 -4
  80. package/src/runtime/config.ts +4 -0
  81. package/src/runtime/streamConfig.ts +4 -0
  82. package/src/services/AppRegistry.ts +94 -25
  83. package/src/services/CloudClientService.ts +70 -24
  84. package/src/services/LocalMiniappRuntime.ts +85 -26
  85. package/src/services/MiniappLauncher.ts +15 -1
  86. package/src/services/NavigationHandlers.ts +8 -0
  87. package/src/services/OtaUpdateCheckService.ts +32 -6
  88. package/src/services/miniappInstallIdentity.ts +18 -0
  89. package/src/services/otaManifestPolicy.ts +11 -0
  90. package/src/services/otaManifestUrl.ts +10 -2
  91. package/src/stores/glasses.ts +6 -0
  92. package/src/stores/settings.ts +20 -3
@@ -34,6 +34,7 @@ export type MentraLiveOtaScreen =
34
34
  | "wifi_required"
35
35
  | "up_to_date"
36
36
  | "dev_build"
37
+ | "unofficial_client"
37
38
  | "check_failed"
38
39
  | "update_info_unavailable"
39
40
  | "starting"
@@ -103,6 +104,8 @@ export type MentraLiveOtaState = {
103
104
  releaseTransition: MentraLiveOtaReleaseTransition | null
104
105
  /** Release notes crossed by this update, newest first. Populated on completion. */
105
106
  changelogs: ReleaseChangelog[]
107
+ /** Sideloaded glasses client package, set only on the `unofficial_client` screen. */
108
+ glassesPackageName: string | null
106
109
  }
107
110
 
108
111
  export type UseMentraLiveOtaOptions = {
@@ -129,7 +132,7 @@ export type MentraLiveOtaController = {
129
132
  openWifiSetup: () => void
130
133
  }
131
134
 
132
- type CheckState = "checking" | "update_available" | "no_update" | "dev_build" | "error"
135
+ type CheckState = "checking" | "update_available" | "no_update" | "dev_build" | "unofficial_client" | "error"
133
136
 
134
137
  const AUTO_CHAIN_NETWORK_RETRY_DELAY_MS = 5000
135
138
  const AUTO_CHAIN_COMPLETE_DELAY_MS = 750
@@ -216,6 +219,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
216
219
  const [isUpdateRequired, setIsUpdateRequired] = useState(true)
217
220
  const [isVersionChange, setIsVersionChange] = useState(false)
218
221
  const [errorKind, setErrorKind] = useState<"network" | "pin_unavailable">("network")
222
+ const [unofficialClientPackage, setUnofficialClientPackage] = useState<string | null>(null)
219
223
  const [updateFingerprint, setUpdateFingerprint] = useState<string | null>(null)
220
224
  const [offeredReleaseTransition, setOfferedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(null)
221
225
  const [completedReleaseTransition, setCompletedReleaseTransition] = useState<MentraLiveOtaReleaseTransition | null>(
@@ -379,6 +383,14 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
379
383
  setCheckState("dev_build")
380
384
  return
381
385
  }
386
+ if (result.skippedReason === "unofficial_client") {
387
+ stopOtaAutoChain()
388
+ checkCompletedRef.current = true
389
+ ota.clearUpdateAvailable()
390
+ setUnofficialClientPackage(result.packageName ?? null)
391
+ setCheckState("unofficial_client")
392
+ return
393
+ }
382
394
  if (!result.hasCheckCompleted) {
383
395
  stopOtaAutoChain()
384
396
  checkCompletedRef.current = true
@@ -642,6 +654,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
642
654
  completedUpdate: false,
643
655
  releaseTransition: null,
644
656
  changelogs: [],
657
+ glassesPackageName: null,
645
658
  }
646
659
  }
647
660
 
@@ -653,6 +666,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
653
666
  screen = wifiRequired ? "wifi_required" : batteryBlocked ? "battery_required" : "update_available"
654
667
  } else if (checkState === "no_update") screen = "up_to_date"
655
668
  else if (checkState === "dev_build") screen = "dev_build"
669
+ else if (checkState === "unofficial_client") screen = "unofficial_client"
656
670
  else screen = errorKind === "pin_unavailable" ? "update_info_unavailable" : "check_failed"
657
671
 
658
672
  let error: MentraLiveOtaError | null = null
@@ -688,7 +702,11 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
688
702
  error,
689
703
  canInstall: screen === "update_available" && canInstall,
690
704
  canRetry: screen === "check_failed",
691
- canFinish: screen === "up_to_date" || screen === "dev_build" || screen === "update_info_unavailable",
705
+ canFinish:
706
+ screen === "up_to_date" ||
707
+ screen === "dev_build" ||
708
+ screen === "unofficial_client" ||
709
+ screen === "update_info_unavailable",
692
710
  canDismiss:
693
711
  (screen === "update_available" || screen === "wifi_required" || screen === "battery_required") &&
694
712
  !isUpdateRequired &&
@@ -706,6 +724,7 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
706
724
  ? completedReleaseTransition
707
725
  : null,
708
726
  changelogs: screen === "up_to_date" ? completedChangelogs : [],
727
+ glassesPackageName: screen === "unofficial_client" ? unofficialClientPackage : null,
709
728
  }
710
729
  }
711
730
 
@@ -768,8 +787,10 @@ export function useMentraLiveOta(options: UseMentraLiveOtaOptions = {}): MentraL
768
787
  completedUpdate: false,
769
788
  releaseTransition: releaseTransitionFromRange(otaAutoChainReleaseRange()),
770
789
  changelogs,
790
+ glassesPackageName: null,
771
791
  }
772
792
  }, [
793
+ unofficialClientPackage,
773
794
  checkState,
774
795
  batteryBlocked,
775
796
  completedChangelogs,
@@ -10,7 +10,11 @@ export type SubjectTokenType = "supabase" | "authing" | (string & {})
10
10
 
11
11
  export interface IslandAuth {
12
12
  /** Returns the host's current (auto-refreshed) subject token for the backend. */
13
- getSubjectToken: () => Promise<{token: string; type: SubjectTokenType}>
13
+ getSubjectToken?: () => Promise<{token: string; type: SubjectTokenType}>
14
+ /** Supplies a token issued directly for Runtime in a Core-free deployment. */
15
+ getRuntimeToken?: (opts?: {forceRefresh?: boolean}) => Promise<string>
16
+ /** Stable, deployment-scoped local identity for bundled miniapp storage. */
17
+ getUserId?: () => Promise<string> | string
14
18
  /**
15
19
  * Optional auth-session listener. Hosts that can emit auth changes should
16
20
  * wire this so engine can reconnect backend sessions after restored login
@@ -21,9 +25,23 @@ export interface IslandAuth {
21
25
 
22
26
  export interface IslandConfigValues {
23
27
  /** cloud-v2 core service base URL (defaults resolved by the cloud client). */
24
- coreUrl?: string
28
+ coreUrl?: string | null
25
29
  /** cloud-v2 runtime service base URL. */
26
- runtimeUrl?: string
30
+ runtimeUrl?: string | null
31
+ /** Open Runtime's live WebSocket/audio session. Defaults to true. */
32
+ runtimeRealtimeSession?: boolean
33
+ /** Complete allowlist for bundled/local miniapps; null or omitted allows all. */
34
+ localMiniappAllowlist?: readonly string[] | null
35
+ /** Provenance-aware policy for workspace SYSTEM and managed miniapps. */
36
+ localMiniappPolicy?: LocalMiniappPolicy
37
+ /** Optional per-package configuration supplied by the host deployment. */
38
+ miniappConfiguration?: Readonly<Record<string, Readonly<Record<string, string>>>>
39
+ /** Deployment-scoped key for the persisted Core refresh token. */
40
+ cloudAuthStorageKey?: string
41
+ /** Deployment-pinned Mentra Live OTA manifest; explicit null disables remote OTA. */
42
+ otaManifestUrl?: string | null
43
+ /** Deployment capability policy. Omitted entries preserve consumer behavior. */
44
+ features?: Partial<Record<IslandFeatureName, boolean>>
27
45
  /** OEM identifier (Mentra is OEM #0); reserved for OEM auth/telemetry. */
28
46
  oemId?: string
29
47
  /**
@@ -38,6 +56,21 @@ export interface IslandConfigValues {
38
56
  devServerHost?: () => string | undefined
39
57
  }
40
58
 
59
+ export interface ManagedMiniappPolicyEntry {
60
+ packageName: string
61
+ version: string
62
+ sha256: string
63
+ deploymentId: string
64
+ deploymentOrigin: string
65
+ }
66
+
67
+ export interface LocalMiniappPolicy {
68
+ systemPackageNames: readonly string[] | null
69
+ managed: readonly ManagedMiniappPolicyEntry[]
70
+ }
71
+
72
+ export type IslandFeatureName = "managedStreams" | "nativeMeetings" | "cloudSpeech" | "onDeviceSpeech" | "navigation"
73
+
41
74
  export type IslandAnalytics = (event: string, props?: Record<string, unknown>) => void
42
75
 
43
76
  /**
@@ -74,6 +107,62 @@ export interface IslandConfigureOptions {
74
107
  ui?: IslandUiSeams
75
108
  }
76
109
 
110
+ export function isLocalMiniappPackageAllowed(packageName: string): boolean {
111
+ const policy = options?.config?.localMiniappPolicy
112
+ if (policy) {
113
+ return (
114
+ policy.systemPackageNames === null ||
115
+ policy.systemPackageNames.includes(packageName) ||
116
+ policy.managed.some((entry) => entry.packageName === packageName)
117
+ )
118
+ }
119
+ const allowlist = options?.config?.localMiniappAllowlist
120
+ return allowlist == null || allowlist.includes(packageName)
121
+ }
122
+
123
+ export function isOfflineSystemMiniappAllowed(packageName: string): boolean {
124
+ const policy = options?.config?.localMiniappPolicy
125
+ if (!policy) return isLocalMiniappPackageAllowed(packageName)
126
+ return policy.systemPackageNames === null || policy.systemPackageNames.includes(packageName)
127
+ }
128
+
129
+ export function isInstalledMiniappAllowed(
130
+ packageName: string,
131
+ version: string | undefined,
132
+ releaseIdentity: {
133
+ source: string
134
+ bundleSha256?: string
135
+ deploymentId?: string
136
+ deploymentOrigin?: string
137
+ } | null,
138
+ ): boolean {
139
+ const policy = options?.config?.localMiniappPolicy
140
+ if (!policy) return isLocalMiniappPackageAllowed(packageName)
141
+
142
+ const systemApproved = policy.systemPackageNames === null || policy.systemPackageNames.includes(packageName)
143
+ if (systemApproved && releaseIdentity?.source === "bundled_asset") return true
144
+ if (!version || releaseIdentity?.source !== "deployment_manifest") return false
145
+
146
+ return policy.managed.some(
147
+ (entry) =>
148
+ entry.packageName === packageName &&
149
+ entry.version === version &&
150
+ entry.sha256 === releaseIdentity.bundleSha256?.toLowerCase() &&
151
+ entry.deploymentId === releaseIdentity.deploymentId &&
152
+ entry.deploymentOrigin === releaseIdentity.deploymentOrigin,
153
+ )
154
+ }
155
+
156
+ /** Read a defensive package-scoped configuration snapshot. */
157
+ export function getMiniappConfiguration(packageName: string): Record<string, string> {
158
+ const configuration = options?.config?.miniappConfiguration?.[packageName]
159
+ return configuration ? {...configuration} : {}
160
+ }
161
+
162
+ export function isFeatureEnabled(feature: IslandFeatureName): boolean {
163
+ return options?.config?.features?.[feature] !== false
164
+ }
165
+
77
166
  let options: IslandConfigureOptions | null = null
78
167
  let started = false
79
168
 
@@ -91,7 +180,31 @@ export function configure(opts: IslandConfigureOptions): void {
91
180
  console.warn("engine.configure() called after engine.start(); ignored — stop() the runtime before reconfiguring")
92
181
  return
93
182
  }
94
- options = opts
183
+ const localMiniappAllowlist = opts.config?.localMiniappAllowlist
184
+ const localMiniappPolicy = opts.config?.localMiniappPolicy
185
+ options = {
186
+ ...opts,
187
+ config: opts.config
188
+ ? {
189
+ ...opts.config,
190
+ localMiniappAllowlist: Array.isArray(localMiniappAllowlist)
191
+ ? Object.freeze([...localMiniappAllowlist])
192
+ : localMiniappAllowlist,
193
+ localMiniappPolicy: localMiniappPolicy
194
+ ? Object.freeze({
195
+ systemPackageNames: Array.isArray(localMiniappPolicy.systemPackageNames)
196
+ ? Object.freeze([...localMiniappPolicy.systemPackageNames])
197
+ : null,
198
+ managed: Object.freeze(
199
+ localMiniappPolicy.managed.map((entry) =>
200
+ Object.freeze({...entry, sha256: entry.sha256.toLowerCase()}),
201
+ ),
202
+ ),
203
+ })
204
+ : undefined,
205
+ }
206
+ : undefined,
207
+ }
95
208
  }
96
209
 
97
210
  /**
@@ -101,6 +101,10 @@ export interface StreamVideoConfig {
101
101
  width?: number
102
102
  height?: number
103
103
  bitrate?: number
104
+ /** WHIP minimum target in bps; omitted leaves it unset. Clamped to the maximum. */
105
+ minBitrateBps?: number
106
+ /** WHIP startup bitrate in bps, clamped to the requested bounds. */
107
+ initialBitrateBps?: number
104
108
  fps?: number
105
109
  }
106
110
 
@@ -12,11 +12,15 @@ export const normalizeStreamVideoConfig = (value: unknown): StreamVideoConfig |
12
12
  const width = finiteNumber(value.width)
13
13
  const height = finiteNumber(value.height)
14
14
  const bitrate = finiteNumber(value.bitrate)
15
+ const minBitrateBps = finiteNumber(value.minBitrateBps)
16
+ const initialBitrateBps = finiteNumber(value.initialBitrateBps)
15
17
  const frameRate = finiteNumber(value.frameRate)
16
18
  const fps = frameRate ?? finiteNumber(value.fps)
17
19
  if (width !== undefined) config.width = width
18
20
  if (height !== undefined) config.height = height
19
21
  if (bitrate !== undefined) config.bitrate = bitrate
22
+ if (minBitrateBps !== undefined) config.minBitrateBps = Math.max(0, Math.trunc(minBitrateBps))
23
+ if (initialBitrateBps !== undefined) config.initialBitrateBps = Math.max(0, Math.trunc(initialBitrateBps))
20
24
  if (fps !== undefined) config.fps = fps
21
25
  return Object.keys(config).length > 0 ? config : undefined
22
26
  }
@@ -28,12 +28,25 @@ import {HardwareRequirement, HardwareRequirementLevel, HardwareType} from "../ty
28
28
  import {configuredDevHost} from "../utils/configuredDevHost"
29
29
  import {storage} from "../utils/storage/storage"
30
30
  import {printDirectory} from "../utils/storage/zip"
31
+ import {isInstalledMiniappAllowed, isOfflineSystemMiniappAllowed} from "../runtime/bootstrap"
31
32
  import {checkManifestVersions} from "./manifestVersionGate"
32
33
  import {normalizeManifestActions} from "./manifestActions"
34
+ import {miniappInstallIdentityError, type MiniappInstallExpectations} from "./miniappInstallIdentity"
33
35
  import {miniappRunningRegistry} from "./MiniappRunningRegistry"
34
36
 
35
37
  export {normalizeManifestActions} from "./manifestActions"
36
38
 
39
+ let installQueue: Promise<void> = Promise.resolve()
40
+
41
+ function serializeInstall<T>(operation: () => Promise<T>): Promise<T> {
42
+ const result = installQueue.then(operation, operation)
43
+ installQueue = result.then(
44
+ () => undefined,
45
+ () => undefined,
46
+ )
47
+ return result
48
+ }
49
+
37
50
  const ALLOWED_PERMISSION_TYPES: ReadonlySet<AppPermissionType> = new Set<AppPermissionType>([
38
51
  "MICROPHONE",
39
52
  "CAMERA",
@@ -141,10 +154,12 @@ interface InstalledLma {
141
154
  }
142
155
 
143
156
  export interface MiniappReleaseIdentity {
144
- source: "direct_download" | "bundled_asset" | "preinstalled_registry" | "dev_snapshot"
157
+ source: "direct_download" | "bundled_asset" | "preinstalled_registry" | "deployment_manifest" | "dev_snapshot"
145
158
  releaseId?: string
146
159
  bundleSha256?: string
147
160
  channel?: string
161
+ deploymentId?: string
162
+ deploymentOrigin?: string
148
163
  }
149
164
 
150
165
  function releaseIdentityKey(packageName: string, version: string): string {
@@ -209,6 +224,7 @@ async function downloadMiniAppZip(url: string): Promise<string> {
209
224
  async function unpackMiniApp(
210
225
  zipPath: string,
211
226
  versionOverride?: string,
227
+ expected?: MiniappInstallExpectations,
212
228
  ): Promise<{packageName: string; version: string}> {
213
229
  const unzipDir = new Directory(Paths.cache, "lma_unzip")
214
230
  try {
@@ -246,6 +262,8 @@ async function unpackMiniApp(
246
262
  throw "READ_MANIFEST_FAILED"
247
263
  }
248
264
  const version = versionOverride ?? manifestVersion
265
+ const identityError = miniappInstallIdentityError({packageName, version: manifestVersion}, expected)
266
+ if (identityError) throw new Error(identityError)
249
267
  console.log(`ZIP: installing ${packageName} as version ${version}`)
250
268
 
251
269
  const basePackageDir = new Directory(Paths.document, "lmas", packageName)
@@ -260,6 +278,9 @@ async function unpackMiniApp(
260
278
 
261
279
  const versionDir = new Directory(basePackageDir, version)
262
280
  try {
281
+ if (expected?.rejectExistingVersion && versionDir.exists) {
282
+ throw new Error(`Miniapp ${packageName}@${version} is already installed`)
283
+ }
263
284
  if (!versionDir.exists) {
264
285
  versionDir.create()
265
286
  } else {
@@ -299,10 +320,11 @@ async function unpackMiniApp(
299
320
  async function downloadAndInstallMiniApp(
300
321
  url: string,
301
322
  versionOverride?: string,
323
+ expected?: MiniappInstallExpectations,
302
324
  ): Promise<{packageName: string; version: string}> {
303
325
  const downloadedZipPath = await downloadMiniAppZip(url)
304
326
  console.log("ZIP: done downloading, starting unzip")
305
- return unpackMiniApp(downloadedZipPath, versionOverride)
327
+ return unpackMiniApp(downloadedZipPath, versionOverride, expected)
306
328
  }
307
329
 
308
330
  type Listener = () => void
@@ -445,17 +467,29 @@ class AppRegistry {
445
467
  */
446
468
  public installFromUrl(
447
469
  url: string,
448
- opts?: {versionOverride?: string; releaseIdentity?: MiniappReleaseIdentity},
470
+ opts?: {
471
+ versionOverride?: string
472
+ releaseIdentity?: MiniappReleaseIdentity
473
+ expectedPackageName?: string
474
+ expectedVersion?: string
475
+ rejectExistingVersion?: boolean
476
+ },
449
477
  ): AsyncResult<void, Error> {
450
- return Res.try_async(async () => {
451
- const {packageName, version} = await downloadAndInstallMiniApp(url, opts?.versionOverride)
452
- console.log("APP_REGISTRY: Downloaded and installed mini app")
453
- this.finalizeInstall(
454
- packageName,
455
- version,
456
- opts?.releaseIdentity ?? {source: version.startsWith("dev-") ? "dev_snapshot" : "direct_download"},
457
- )
458
- })
478
+ return Res.try_async(() =>
479
+ serializeInstall(async () => {
480
+ const {packageName, version} = await downloadAndInstallMiniApp(url, opts?.versionOverride, {
481
+ packageName: opts?.expectedPackageName,
482
+ version: opts?.expectedVersion,
483
+ rejectExistingVersion: opts?.rejectExistingVersion,
484
+ })
485
+ console.log("APP_REGISTRY: Downloaded and installed mini app")
486
+ this.finalizeInstall(
487
+ packageName,
488
+ version,
489
+ opts?.releaseIdentity ?? {source: version.startsWith("dev-") ? "dev_snapshot" : "direct_download"},
490
+ )
491
+ }),
492
+ )
459
493
  }
460
494
 
461
495
  /**
@@ -467,14 +501,26 @@ class AppRegistry {
467
501
  */
468
502
  public installFromLocalZip(
469
503
  zipPath: string,
470
- opts?: {versionOverride?: string; releaseIdentity?: MiniappReleaseIdentity},
504
+ opts?: {
505
+ versionOverride?: string
506
+ releaseIdentity?: MiniappReleaseIdentity
507
+ expectedPackageName?: string
508
+ expectedVersion?: string
509
+ rejectExistingVersion?: boolean
510
+ },
471
511
  ): AsyncResult<{packageName: string; version: string}, Error> {
472
- return Res.try_async(async () => {
473
- const {packageName, version} = await unpackMiniApp(zipPath, opts?.versionOverride)
474
- console.log("APP_REGISTRY: Installed mini app from local zip")
475
- this.finalizeInstall(packageName, version, opts?.releaseIdentity ?? {source: "bundled_asset"})
476
- return {packageName, version}
477
- })
512
+ return Res.try_async(() =>
513
+ serializeInstall(async () => {
514
+ const {packageName, version} = await unpackMiniApp(zipPath, opts?.versionOverride, {
515
+ packageName: opts?.expectedPackageName,
516
+ version: opts?.expectedVersion,
517
+ rejectExistingVersion: opts?.rejectExistingVersion,
518
+ })
519
+ console.log("APP_REGISTRY: Installed mini app from local zip")
520
+ this.finalizeInstall(packageName, version, opts?.releaseIdentity ?? {source: "bundled_asset"})
521
+ return {packageName, version}
522
+ }),
523
+ )
478
524
  }
479
525
 
480
526
  /**
@@ -504,6 +550,22 @@ class AppRegistry {
504
550
  return result.is_ok() ? result.value : null
505
551
  }
506
552
 
553
+ /** Enumerate installed releases carrying deployment ownership metadata. */
554
+ public getDeploymentOwnedReleases(): Array<{
555
+ packageName: string
556
+ version: string
557
+ identity: MiniappReleaseIdentity
558
+ }> {
559
+ const releases: Array<{packageName: string; version: string; identity: MiniappReleaseIdentity}> = []
560
+ for (const packageName of this.getPackageNames()) {
561
+ for (const version of this.getInstalledVersions(packageName)) {
562
+ const identity = this.getReleaseIdentity(packageName, version)
563
+ if (identity?.source === "deployment_manifest") releases.push({packageName, version, identity})
564
+ }
565
+ }
566
+ return releases
567
+ }
568
+
507
569
  public installFromJsonUrl(baseUrl: string): AsyncResult<{packageName: string; version: string; name: string}, Error> {
508
570
  return Res.try_async(async () => {
509
571
  const trimmed = baseUrl.replace(/\/$/, "")
@@ -732,12 +794,21 @@ class AppRegistry {
732
794
  * registration in finalizeInstall. Native offline apps keep top priority.
733
795
  */
734
796
  private mergeProjectedApps(diskApps: ClientApp[]): ClientApp[] {
735
- const offline = this.projectOfflineApps()
797
+ const offline = this.projectOfflineApps().filter((app) => isOfflineSystemMiniappAllowed(app.packageName))
736
798
  const offlinePackages = new Set(offline.map((app) => app.packageName))
737
- const dev = this.projectDevApps().filter((app) => !offlinePackages.has(app.packageName))
799
+ const dev = this.projectDevApps().filter(
800
+ (app) => isInstalledMiniappAllowed(app.packageName, undefined, null) && !offlinePackages.has(app.packageName),
801
+ )
738
802
  const devPackages = new Set(dev.map((app) => app.packageName))
739
803
  const installed = diskApps.filter(
740
- (app) => !offlinePackages.has(app.packageName) && !devPackages.has(app.packageName),
804
+ (app) =>
805
+ isInstalledMiniappAllowed(
806
+ app.packageName,
807
+ app.version,
808
+ app.version ? this.getReleaseIdentity(app.packageName, app.version) : null,
809
+ ) &&
810
+ !offlinePackages.has(app.packageName) &&
811
+ !devPackages.has(app.packageName),
741
812
  )
742
813
  return [...installed, ...dev, ...offline]
743
814
  }
@@ -1003,7 +1074,6 @@ export interface DevAppRecord {
1003
1074
  const DEV_APPS_INDEX_KEY = "dev_apps_index"
1004
1075
  const DEV_APP_ICONS_DIR = "dev-miniapp-icons"
1005
1076
 
1006
-
1007
1077
  function isPrivateLanHost(hostname: string): boolean {
1008
1078
  return (
1009
1079
  hostname === "localhost" ||
@@ -1167,8 +1237,7 @@ export async function registerDevApp(record: DevAppRecord): Promise<void> {
1167
1237
  // Relaunch paths (developer-URL screen, loadDevMiniapp) omit mdnsHost, so an
1168
1238
  // omitted field keeps whatever the QR scan stored instead of wiping the
1169
1239
  // `.local` failover host.
1170
- const mdnsHost =
1171
- record.mdnsHost !== undefined ? record.mdnsHost.trim() || undefined : readStoredMdnsHost(packageName)
1240
+ const mdnsHost = record.mdnsHost !== undefined ? record.mdnsHost.trim() || undefined : readStoredMdnsHost(packageName)
1172
1241
  const devRecord: DevAppRecord = {
1173
1242
  ...record,
1174
1243
  packageName,