@glassly/engine 0.2.9-dev.457 → 0.3.0-beta.464

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 (75) hide show
  1. package/build/engine.d.ts +3 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/facades/miniapps.d.ts +6 -0
  4. package/build/facades/miniapps.d.ts.map +1 -1
  5. package/build/facades/miniapps.js +7 -0
  6. package/build/facades/miniapps.js.map +1 -1
  7. package/build/generated/releaseMetadata.js +4 -4
  8. package/build/generated/releaseMetadata.js.map +1 -1
  9. package/build/index.d.ts +2 -2
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +2 -2
  12. package/build/index.js.map +1 -1
  13. package/build/services/AppRegistry.js +2 -2
  14. package/build/services/AppRegistry.js.map +1 -1
  15. package/build/services/BlobStore.js +2 -2
  16. package/build/services/BlobStore.js.map +1 -1
  17. package/build/services/LocalMiniappRuntime.d.ts +22 -9
  18. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  19. package/build/services/LocalMiniappRuntime.js +104 -49
  20. package/build/services/LocalMiniappRuntime.js.map +1 -1
  21. package/build/services/MiniappLauncher.d.ts +2 -0
  22. package/build/services/MiniappLauncher.d.ts.map +1 -1
  23. package/build/services/MiniappLauncher.js +6 -1
  24. package/build/services/MiniappLauncher.js.map +1 -1
  25. package/build/services/miniappPermissions.d.ts +16 -0
  26. package/build/services/miniappPermissions.d.ts.map +1 -0
  27. package/build/services/miniappPermissions.js +7 -0
  28. package/build/services/miniappPermissions.js.map +1 -0
  29. package/build/services/pages/OverlayPages.d.ts +13 -2
  30. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  31. package/build/services/pages/OverlayPages.js +11 -10
  32. package/build/services/pages/OverlayPages.js.map +1 -1
  33. package/build/services/pages/PageHost.d.ts +1 -1
  34. package/build/services/pages/PageHost.d.ts.map +1 -1
  35. package/build/services/pages/PageHost.js +2 -2
  36. package/build/services/pages/PageHost.js.map +1 -1
  37. package/build/services/pages/activeDashboard.d.ts +7 -0
  38. package/build/services/pages/activeDashboard.d.ts.map +1 -0
  39. package/build/services/pages/activeDashboard.js +21 -0
  40. package/build/services/pages/activeDashboard.js.map +1 -0
  41. package/build/services/pages/overlayApps.d.ts +20 -13
  42. package/build/services/pages/overlayApps.d.ts.map +1 -1
  43. package/build/services/pages/overlayApps.js +50 -31
  44. package/build/services/pages/overlayApps.js.map +1 -1
  45. package/build/services/settingsBridge.d.ts +15 -14
  46. package/build/services/settingsBridge.d.ts.map +1 -1
  47. package/build/services/settingsBridge.js +20 -20
  48. package/build/services/settingsBridge.js.map +1 -1
  49. package/build/stores/apps.d.ts +5 -0
  50. package/build/stores/apps.d.ts.map +1 -1
  51. package/build/stores/apps.js +40 -2
  52. package/build/stores/apps.js.map +1 -1
  53. package/build/stores/settings.d.ts.map +1 -1
  54. package/build/stores/settings.js +6 -45
  55. package/build/stores/settings.js.map +1 -1
  56. package/build/types/applet.d.ts +2 -2
  57. package/build/types/applet.d.ts.map +1 -1
  58. package/build/types/applet.js.map +1 -1
  59. package/package.json +7 -7
  60. package/src/facades/miniapps.ts +9 -0
  61. package/src/generated/releaseMetadata.ts +4 -4
  62. package/src/index.ts +3 -1
  63. package/src/services/AppRegistry.ts +2 -2
  64. package/src/services/BlobStore.ts +2 -2
  65. package/src/services/LocalMiniappRuntime.ts +124 -49
  66. package/src/services/MiniappLauncher.ts +6 -1
  67. package/src/services/miniappPermissions.ts +19 -0
  68. package/src/services/pages/OverlayPages.ts +11 -10
  69. package/src/services/pages/PageHost.ts +2 -2
  70. package/src/services/pages/activeDashboard.ts +24 -0
  71. package/src/services/pages/overlayApps.ts +51 -30
  72. package/src/services/settingsBridge.ts +20 -20
  73. package/src/stores/apps.ts +44 -2
  74. package/src/stores/settings.ts +6 -45
  75. package/src/types/applet.ts +3 -1
@@ -42,7 +42,9 @@ import dashboardOpenTrace from "./debug/DashboardOpenTrace";
42
42
  import { isAppForegrounded, isPhoneActive, onAppForegroundChange, onPhoneActivityChange } from "./PhoneActivity";
43
43
  import { installedNotificationApps } from "./PhoneNotificationsSync";
44
44
  import { DERIVED_SETTINGS_KEYS, SETTINGS_BRIDGE_KEYS, isDerivedSettingsKey, ringBatteryFrom, selectSettingsBridgeKeys, validateSettingsSet, } from "./settingsBridge";
45
- import { addOverlayApp, displayViewsFor, isOverlayApp, removeOverlayApp, setOverlayViewer } from "./pages/overlayApps";
45
+ import { mayUsePermission } from "./miniappPermissions";
46
+ import { activeDashboardAmong } from "./pages/activeDashboard";
47
+ import { activeDashboard, addDashboardApp, displayViewsFor, isDashboardApp, isOverlayApp, onOverlayAppsChange, removeDashboardApp, setActiveDashboard, setOverlayViewer, } from "./pages/overlayApps";
46
48
  import { pagesEligible } from "./pages/pageEligibility";
47
49
  import nativeNavigationService from "./NativeNavigationService";
48
50
  import headingService from "./HeadingService";
@@ -130,9 +132,6 @@ const SYSTEM_MINIAPP_PACKAGES = new Set([
130
132
  "com.glassly.ai",
131
133
  "com.glassly.feedback",
132
134
  "com.glassly.miniappdev",
133
- // The dashboard: the settings bridge and the miniapp list/start/stop are its side menu and settings
134
- // page, and the notification app list is its notification settings' per-app filter.
135
- "com.glassly.dashboard",
136
135
  ]);
137
136
  const PING_INTERVAL_MS = 5_000;
138
137
  // Min gap between "transcription limit reached" notices on the glasses. A
@@ -214,6 +213,8 @@ const PERMISSION_TYPE_TO_CANONICAL = {
214
213
  MEMORY: "memory",
215
214
  AI: "ai",
216
215
  SPEAKERS: "speakers",
216
+ MANAGE_MINIAPPS: "manageMiniapps",
217
+ PLATFORM_SETTINGS: "platformSettings",
217
218
  };
218
219
  /**
219
220
  * User API keys (Settings > API Keys) a miniapp may read via
@@ -313,6 +314,8 @@ const ALL_CANONICAL_PERMISSIONS = [
313
314
  "ai",
314
315
  "speakers",
315
316
  "displayMirror",
317
+ "manageMiniapps",
318
+ "platformSettings",
316
319
  ];
317
320
  /**
318
321
  * Build the {location, microphone, camera, notifications, calendar, apiKeys}
@@ -961,11 +964,11 @@ class LocalMiniappRuntime {
961
964
  // Drop this app's transcription hints; the cloud subscription rebuild
962
965
  // below (via replaceStreamSubscribers) re-derives the hint union without it.
963
966
  this.transcriptionHintsByApp.delete(packageName);
964
- // An overlay app is going: release the slot if it holds it, and drop the
967
+ // A dashboard is going: release the slot if it holds it, and drop the
965
968
  // dashboard view from its capabilities record.
966
- if (isOverlayApp(packageName)) {
969
+ if (isDashboardApp(packageName)) {
967
970
  overlayPages.remove(packageName);
968
- removeOverlayApp(packageName);
971
+ removeDashboardApp(packageName);
969
972
  }
970
973
  // Its gesture claims go back to whoever claimed them before.
971
974
  this.applyGestureClaimChanges(dropGestureClaims(this.gestureClaims, packageName));
@@ -1478,10 +1481,12 @@ class LocalMiniappRuntime {
1478
1481
  // Update lastPongAt so it doesn't time out right away
1479
1482
  existing.lastPongAt = Date.now();
1480
1483
  existing.unansweredPingRounds = 0;
1481
- // A system_dashboard app may take the overlay slot: its capabilities
1482
- // carry the dashboard view from the start.
1483
- if (existing.installedManifest?.type === "system_dashboard")
1484
- addOverlayApp(packageName);
1484
+ // The dashboard that holds the role may take the overlay slot: its
1485
+ // capabilities carry the dashboard view from the start.
1486
+ if (existing.installedManifest?.type === "dashboard") {
1487
+ addDashboardApp(packageName);
1488
+ this.syncActiveDashboard();
1489
+ }
1485
1490
  const capabilities = this.capabilitiesFor(packageName);
1486
1491
  // Build the declared-permission record for the SDK's session.permissions
1487
1492
  // module. Lower-cased to match v3's PermissionType union (microphone,
@@ -1836,12 +1841,9 @@ class LocalMiniappRuntime {
1836
1841
  return;
1837
1842
  }
1838
1843
  }
1839
- // The settings stream carries the wearer's settings: system apps only.
1840
- if (streams.includes(MiniappStreamType.SETTINGS_CHANGED) && !this.isSystemPackage(packageName)) {
1841
- this.sendResult(packageName, requestId, false, undefined, {
1842
- code: MiniappErrorCode.NOT_PERMITTED,
1843
- message: "The settings stream is restricted to system apps",
1844
- });
1844
+ // The settings stream carries the wearer's settings.
1845
+ if (streams.includes(MiniappStreamType.SETTINGS_CHANGED) &&
1846
+ !this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to subscribe to the settings stream")) {
1845
1847
  return;
1846
1848
  }
1847
1849
  // Add new subscriptions
@@ -1850,8 +1852,9 @@ class LocalMiniappRuntime {
1850
1852
  app.forceLocalTranscriptionStreams = new Set([...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1851
1853
  app.cloudTranscriptionStreams = new Set([...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1852
1854
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
1853
- // An overlay app that listens to phone notifications presents them on the glasses.
1854
- if (isOverlayApp(packageName)) {
1855
+ // The overlay app presents phone notifications on the glasses while it
1856
+ // listens to them; recorded for every dashboard so the role can move.
1857
+ if (isDashboardApp(packageName)) {
1855
1858
  setOverlayViewer(packageName, app.subscriptions.has(MiniappStreamType.PHONE_NOTIFICATION));
1856
1859
  }
1857
1860
  this.recomputeMicRequirements();
@@ -2073,13 +2076,13 @@ class LocalMiniappRuntime {
2073
2076
  });
2074
2077
  return;
2075
2078
  }
2076
- // The dashboard view is the overlay apps' alone; anyone else's frame
2079
+ // The dashboard view is the overlay app's alone; anyone else's frame
2077
2080
  // is dropped, reported rather than silently lost.
2078
2081
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
2079
2082
  this.sendResult(packageName, requestId, true, {
2080
2083
  status: "displayed",
2081
2084
  presented: false,
2082
- reason: "not the dashboard app",
2085
+ reason: "not the active dashboard",
2083
2086
  });
2084
2087
  return;
2085
2088
  }
@@ -2098,10 +2101,11 @@ class LocalMiniappRuntime {
2098
2101
  }
2099
2102
  }
2100
2103
  /**
2101
- * display.showView: the designated dashboard app takes or releases the
2102
- * overlay slot. Refused (NOT_PERMITTED) for any other app; "unsupported"
2103
- * while the glasses cannot show the dashboard view. Idempotent either way;
2104
- * the slot's state comes back to the app as a display_view change.
2104
+ * display.showView: the active dashboard takes or releases the overlay
2105
+ * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
2106
+ * "unsupported" for a dashboard without the role and while the glasses
2107
+ * cannot show the dashboard view. Idempotent either way; the slot's state
2108
+ * comes back to the app as a display_view change.
2105
2109
  */
2106
2110
  handleShowView(packageName, payload, requestId) {
2107
2111
  if (!this.connectedApps.has(packageName)) {
@@ -2116,13 +2120,17 @@ class LocalMiniappRuntime {
2116
2120
  });
2117
2121
  return;
2118
2122
  }
2119
- if (!isOverlayApp(packageName)) {
2123
+ if (!isDashboardApp(packageName)) {
2120
2124
  this.sendResult(packageName, requestId, false, undefined, {
2121
2125
  code: MiniappErrorCode.NOT_PERMITTED,
2122
- message: "The dashboard view belongs to system_dashboard apps",
2126
+ message: 'The dashboard view belongs to miniapps of type "dashboard"',
2123
2127
  });
2124
2128
  return;
2125
2129
  }
2130
+ if (!isOverlayApp(packageName)) {
2131
+ this.sendResult(packageName, requestId, true, { status: "unsupported", reason: "not the active dashboard" });
2132
+ return;
2133
+ }
2126
2134
  if (view === "main") {
2127
2135
  overlayPages.hide(packageName);
2128
2136
  this.sendResult(packageName, requestId, true, { status: "shown" });
@@ -2141,18 +2149,31 @@ class LocalMiniappRuntime {
2141
2149
  dashboardOpenTrace.slotGranted();
2142
2150
  this.sendResult(packageName, requestId, true, shown ? { status: "shown" } : { status: "unsupported", reason: "the overlay slot was refused" });
2143
2151
  }
2144
- // ---------------------------------------------------------------------------
2145
- // Settings bridge (session.settings) — system apps only
2146
- // ---------------------------------------------------------------------------
2147
- requireSystemForSettings(packageName, requestId) {
2148
- if (this.isSystemPackage(packageName))
2152
+ /**
2153
+ * Reject the request with PERMISSION_NOT_DECLARED unless the caller's
2154
+ * manifest declares `permission`. System apps pass without declaring it.
2155
+ */
2156
+ requirePermission(packageName, requestId, permission, context) {
2157
+ const caller = {
2158
+ system: this.isSystemPackage(packageName),
2159
+ permissions: this.connectedApps.get(packageName)?.installedManifest?.permissions,
2160
+ };
2161
+ if (mayUsePermission(caller, permission))
2149
2162
  return true;
2163
+ logPermissionNotDeclared(packageName, permission, context, `{"type": "${permission}"}`);
2150
2164
  this.sendResult(packageName, requestId, false, undefined, {
2151
- code: MiniappErrorCode.NOT_PERMITTED,
2152
- message: "The settings bridge is restricted to system apps",
2165
+ code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2166
+ message: `${permission} permission not declared in miniapp.json. Add {"type": "${permission}"} to the "permissions" array.`,
2167
+ permission,
2153
2168
  });
2154
2169
  return false;
2155
2170
  }
2171
+ // ---------------------------------------------------------------------------
2172
+ // Settings bridge (session.settings) — PLATFORM_SETTINGS permission
2173
+ // ---------------------------------------------------------------------------
2174
+ requireSettingsPermission(packageName, requestId) {
2175
+ return this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to use the settings bridge");
2176
+ }
2156
2177
  /** A bridged key's current value: the store's, or the engine's for a derived key. */
2157
2178
  settingsBridgeValue(key) {
2158
2179
  if (isDerivedSettingsKey(key))
@@ -2167,10 +2188,12 @@ class LocalMiniappRuntime {
2167
2188
  return localDisplayManager.foregroundAppOnScreen();
2168
2189
  case "ring_battery":
2169
2190
  return ringBatteryFrom(useGlassesStore.getState());
2191
+ case "active_dashboard":
2192
+ return activeDashboard();
2170
2193
  }
2171
2194
  }
2172
2195
  handleSettingsGet(packageName, payload, requestId) {
2173
- if (!this.requireSystemForSettings(packageName, requestId))
2196
+ if (!this.requireSettingsPermission(packageName, requestId))
2174
2197
  return;
2175
2198
  const values = {};
2176
2199
  for (const key of selectSettingsBridgeKeys(payload.keys))
@@ -2178,7 +2201,7 @@ class LocalMiniappRuntime {
2178
2201
  this.sendResult(packageName, requestId, true, { values });
2179
2202
  }
2180
2203
  async handleSettingsSet(packageName, payload, requestId) {
2181
- if (!this.requireSystemForSettings(packageName, requestId))
2204
+ if (!this.requireSettingsPermission(packageName, requestId))
2182
2205
  return;
2183
2206
  const validated = validateSettingsSet(payload);
2184
2207
  if (!validated.ok) {
@@ -2198,8 +2221,9 @@ class LocalMiniappRuntime {
2198
2221
  this.sendResult(packageName, requestId, true);
2199
2222
  }
2200
2223
  async handleNotificationAppsList(packageName, requestId) {
2201
- if (!this.requireSystemForSettings(packageName, requestId))
2224
+ if (!this.requirePermission(packageName, requestId, "READ_NOTIFICATIONS", "to list the apps that post notifications")) {
2202
2225
  return;
2226
+ }
2203
2227
  try {
2204
2228
  this.sendResult(packageName, requestId, true, await installedNotificationApps());
2205
2229
  }
@@ -2213,8 +2237,9 @@ class LocalMiniappRuntime {
2213
2237
  /**
2214
2238
  * Push bridged settings as they change: stored keys from the store, the
2215
2239
  * derived keys whenever their inputs move (phone activity, the display,
2216
- * the apps store, the ring). Subscribers are system apps only (gated at
2217
- * SUBSCRIBE), so this is a no-op fan-out for everyone else.
2240
+ * the apps store, the ring, the dashboard role). Subscribers declared
2241
+ * PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
2242
+ * everyone else.
2218
2243
  */
2219
2244
  ensureSettingsBridgeWired() {
2220
2245
  if (this.settingsBridgeWired)
@@ -2249,6 +2274,7 @@ class LocalMiniappRuntime {
2249
2274
  onAppForegroundChange((foregrounded) => this.broadcastVisibility(foregrounded));
2250
2275
  localDisplayManager.onActiveDisplayChange(pushDerived);
2251
2276
  useAppStatusStore.subscribe(pushDerived);
2277
+ onOverlayAppsChange(pushDerived);
2252
2278
  useGlassesStore.subscribe((s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`, pushDerived);
2253
2279
  }
2254
2280
  /**
@@ -4562,6 +4588,33 @@ class LocalMiniappRuntime {
4562
4588
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast);
4563
4589
  // The dashboard master switch flips the dashboard view.
4564
4590
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast);
4591
+ // The wearer's choice and the installed dashboards decide who holds the role.
4592
+ const syncRole = () => this.syncActiveDashboard();
4593
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole);
4594
+ useAppStatusStore.subscribe(syncRole);
4595
+ syncRole();
4596
+ }
4597
+ /**
4598
+ * Recompute who holds the dashboard role (ADR-0009). The previous holder
4599
+ * gives up the slot first, so it hears the view change while it is still
4600
+ * the overlay app; then every app sees its views again.
4601
+ */
4602
+ syncActiveDashboard() {
4603
+ const candidates = [...useAppStatusStore.getState().apps];
4604
+ // A dashboard can connect at launch before the apps store has listed it.
4605
+ for (const [packageName, app] of this.connectedApps) {
4606
+ if (candidates.some((candidate) => candidate.packageName === packageName))
4607
+ continue;
4608
+ candidates.push({ packageName, type: app.installedManifest?.type });
4609
+ }
4610
+ const next = activeDashboardAmong(candidates);
4611
+ const previous = activeDashboard();
4612
+ if (next === previous)
4613
+ return;
4614
+ if (previous !== null)
4615
+ overlayPages.hide(previous, "replaced");
4616
+ setActiveDashboard(next);
4617
+ this.broadcastCapabilities();
4565
4618
  }
4566
4619
  /** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
4567
4620
  capabilitiesFor(packageName) {
@@ -4988,14 +5041,16 @@ class LocalMiniappRuntime {
4988
5041
  /* telemetry must never break an interop call */
4989
5042
  }
4990
5043
  }
4991
- /** Reject the request with NOT_PERMITTED unless the caller is a system app. */
4992
- requireSystemCaller(packageName, requestId, op, target) {
4993
- if (this.isSystemPackage(packageName))
5044
+ /** Reject the request unless the caller may list, start and stop other miniapps (MANAGE_MINIAPPS). */
5045
+ requireMiniappsPermission(packageName, requestId, op, target) {
5046
+ if (this.requirePermission(packageName, requestId, "MANAGE_MINIAPPS", `to ${op} miniapps`))
4994
5047
  return true;
4995
- this.auditInterop({ caller: packageName, op, target, ok: false, errorCode: MiniappErrorCode.NOT_PERMITTED });
4996
- this.sendResult(packageName, requestId, false, undefined, {
4997
- code: MiniappErrorCode.NOT_PERMITTED,
4998
- message: "Inter-miniapp APIs are restricted to system apps",
5048
+ this.auditInterop({
5049
+ caller: packageName,
5050
+ op,
5051
+ target,
5052
+ ok: false,
5053
+ errorCode: MiniappErrorCode.PERMISSION_NOT_DECLARED,
4999
5054
  });
5000
5055
  return false;
5001
5056
  }
@@ -5018,7 +5073,7 @@ class LocalMiniappRuntime {
5018
5073
  };
5019
5074
  }
5020
5075
  handleMiniappsList(packageName, payload, requestId) {
5021
- if (!this.requireSystemCaller(packageName, requestId, "list"))
5076
+ if (!this.requireMiniappsPermission(packageName, requestId, "list"))
5022
5077
  return;
5023
5078
  const includeIncompatible = payload.includeIncompatible === true;
5024
5079
  const infos = this.interopApps()
@@ -5032,7 +5087,7 @@ class LocalMiniappRuntime {
5032
5087
  }
5033
5088
  async handleMiniappsStart(packageName, payload, requestId) {
5034
5089
  const target = payload.packageName;
5035
- if (!this.requireSystemCaller(packageName, requestId, "start", target))
5090
+ if (!this.requireMiniappsPermission(packageName, requestId, "start", target))
5036
5091
  return;
5037
5092
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined;
5038
5093
  if (!target || !app) {
@@ -5121,7 +5176,7 @@ class LocalMiniappRuntime {
5121
5176
  }
5122
5177
  async handleMiniappsStop(packageName, payload, requestId) {
5123
5178
  const target = payload.packageName;
5124
- if (!this.requireSystemCaller(packageName, requestId, "stop", target))
5179
+ if (!this.requireMiniappsPermission(packageName, requestId, "stop", target))
5125
5180
  return;
5126
5181
  if (!target) {
5127
5182
  this.sendResult(packageName, requestId, false, undefined, {