@glassly/engine 0.3.0-dev.462 → 0.3.0-dev.469

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 (72) 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 +3 -3
  8. package/build/generated/releaseMetadata.js.map +1 -1
  9. package/build/index.d.ts +1 -1
  10. package/build/index.d.ts.map +1 -1
  11. package/build/index.js +1 -1
  12. package/build/index.js.map +1 -1
  13. package/build/services/AppRegistry.js +1 -1
  14. package/build/services/AppRegistry.js.map +1 -1
  15. package/build/services/LocalMiniappRuntime.d.ts +22 -9
  16. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  17. package/build/services/LocalMiniappRuntime.js +108 -50
  18. package/build/services/LocalMiniappRuntime.js.map +1 -1
  19. package/build/services/MiniappLauncher.d.ts +2 -0
  20. package/build/services/MiniappLauncher.d.ts.map +1 -1
  21. package/build/services/MiniappLauncher.js +6 -1
  22. package/build/services/MiniappLauncher.js.map +1 -1
  23. package/build/services/miniappPermissions.d.ts +16 -0
  24. package/build/services/miniappPermissions.d.ts.map +1 -0
  25. package/build/services/miniappPermissions.js +7 -0
  26. package/build/services/miniappPermissions.js.map +1 -0
  27. package/build/services/pages/OverlayPages.d.ts +13 -2
  28. package/build/services/pages/OverlayPages.d.ts.map +1 -1
  29. package/build/services/pages/OverlayPages.js +11 -10
  30. package/build/services/pages/OverlayPages.js.map +1 -1
  31. package/build/services/pages/PageHost.d.ts +1 -1
  32. package/build/services/pages/PageHost.d.ts.map +1 -1
  33. package/build/services/pages/PageHost.js +2 -2
  34. package/build/services/pages/PageHost.js.map +1 -1
  35. package/build/services/pages/activeDashboard.d.ts +7 -0
  36. package/build/services/pages/activeDashboard.d.ts.map +1 -0
  37. package/build/services/pages/activeDashboard.js +21 -0
  38. package/build/services/pages/activeDashboard.js.map +1 -0
  39. package/build/services/pages/overlayApps.d.ts +20 -13
  40. package/build/services/pages/overlayApps.d.ts.map +1 -1
  41. package/build/services/pages/overlayApps.js +50 -31
  42. package/build/services/pages/overlayApps.js.map +1 -1
  43. package/build/services/settingsBridge.d.ts +15 -14
  44. package/build/services/settingsBridge.d.ts.map +1 -1
  45. package/build/services/settingsBridge.js +20 -20
  46. package/build/services/settingsBridge.js.map +1 -1
  47. package/build/stores/apps.d.ts +5 -0
  48. package/build/stores/apps.d.ts.map +1 -1
  49. package/build/stores/apps.js +40 -2
  50. package/build/stores/apps.js.map +1 -1
  51. package/build/stores/settings.d.ts.map +1 -1
  52. package/build/stores/settings.js +19 -45
  53. package/build/stores/settings.js.map +1 -1
  54. package/build/types/applet.d.ts +2 -2
  55. package/build/types/applet.d.ts.map +1 -1
  56. package/build/types/applet.js.map +1 -1
  57. package/package.json +7 -7
  58. package/src/facades/miniapps.ts +9 -0
  59. package/src/generated/releaseMetadata.ts +3 -3
  60. package/src/index.ts +2 -0
  61. package/src/services/AppRegistry.ts +1 -1
  62. package/src/services/LocalMiniappRuntime.ts +129 -50
  63. package/src/services/MiniappLauncher.ts +6 -1
  64. package/src/services/miniappPermissions.ts +19 -0
  65. package/src/services/pages/OverlayPages.ts +11 -10
  66. package/src/services/pages/PageHost.ts +2 -2
  67. package/src/services/pages/activeDashboard.ts +24 -0
  68. package/src/services/pages/overlayApps.ts +51 -30
  69. package/src/services/settingsBridge.ts +20 -20
  70. package/src/stores/apps.ts +44 -2
  71. package/src/stores/settings.ts +19 -45
  72. 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));
@@ -1324,7 +1327,10 @@ class LocalMiniappRuntime {
1324
1327
  this.handleOpenUrl(packageName, payload);
1325
1328
  break;
1326
1329
  case MiniappRequestType.DISPLAY_DIAGNOSTICS: {
1327
- const enabled = useSettingsStore.getState().getSetting(SETTINGS.debug_console.key) === true;
1330
+ // The records are parsed out of the G2PERF lines, which the SDK only logs while g2_perf_logs is on.
1331
+ const settings = useSettingsStore.getState();
1332
+ const enabled = settings.getSetting(SETTINGS.debug_console.key) === true &&
1333
+ settings.getSetting(SETTINGS.g2_perf_logs.key) === true;
1328
1334
  this.sendResult(packageName, requestId, true, {
1329
1335
  enabled,
1330
1336
  generatedAt: Date.now(),
@@ -1478,10 +1484,12 @@ class LocalMiniappRuntime {
1478
1484
  // Update lastPongAt so it doesn't time out right away
1479
1485
  existing.lastPongAt = Date.now();
1480
1486
  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);
1487
+ // The dashboard that holds the role may take the overlay slot: its
1488
+ // capabilities carry the dashboard view from the start.
1489
+ if (existing.installedManifest?.type === "dashboard") {
1490
+ addDashboardApp(packageName);
1491
+ this.syncActiveDashboard();
1492
+ }
1485
1493
  const capabilities = this.capabilitiesFor(packageName);
1486
1494
  // Build the declared-permission record for the SDK's session.permissions
1487
1495
  // module. Lower-cased to match v3's PermissionType union (microphone,
@@ -1836,12 +1844,9 @@ class LocalMiniappRuntime {
1836
1844
  return;
1837
1845
  }
1838
1846
  }
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
- });
1847
+ // The settings stream carries the wearer's settings.
1848
+ if (streams.includes(MiniappStreamType.SETTINGS_CHANGED) &&
1849
+ !this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to subscribe to the settings stream")) {
1845
1850
  return;
1846
1851
  }
1847
1852
  // Add new subscriptions
@@ -1850,8 +1855,9 @@ class LocalMiniappRuntime {
1850
1855
  app.forceLocalTranscriptionStreams = new Set([...forceLocalTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1851
1856
  app.cloudTranscriptionStreams = new Set([...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)));
1852
1857
  this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions);
1853
- // An overlay app that listens to phone notifications presents them on the glasses.
1854
- if (isOverlayApp(packageName)) {
1858
+ // The overlay app presents phone notifications on the glasses while it
1859
+ // listens to them; recorded for every dashboard so the role can move.
1860
+ if (isDashboardApp(packageName)) {
1855
1861
  setOverlayViewer(packageName, app.subscriptions.has(MiniappStreamType.PHONE_NOTIFICATION));
1856
1862
  }
1857
1863
  this.recomputeMicRequirements();
@@ -2073,13 +2079,13 @@ class LocalMiniappRuntime {
2073
2079
  });
2074
2080
  return;
2075
2081
  }
2076
- // The dashboard view is the overlay apps' alone; anyone else's frame
2082
+ // The dashboard view is the overlay app's alone; anyone else's frame
2077
2083
  // is dropped, reported rather than silently lost.
2078
2084
  if (payload.view === "dashboard" && !isOverlayApp(packageName)) {
2079
2085
  this.sendResult(packageName, requestId, true, {
2080
2086
  status: "displayed",
2081
2087
  presented: false,
2082
- reason: "not the dashboard app",
2088
+ reason: "not the active dashboard",
2083
2089
  });
2084
2090
  return;
2085
2091
  }
@@ -2098,10 +2104,11 @@ class LocalMiniappRuntime {
2098
2104
  }
2099
2105
  }
2100
2106
  /**
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.
2107
+ * display.showView: the active dashboard takes or releases the overlay
2108
+ * slot. Refused (NOT_PERMITTED) for an app that is not a dashboard;
2109
+ * "unsupported" for a dashboard without the role and while the glasses
2110
+ * cannot show the dashboard view. Idempotent either way; the slot's state
2111
+ * comes back to the app as a display_view change.
2105
2112
  */
2106
2113
  handleShowView(packageName, payload, requestId) {
2107
2114
  if (!this.connectedApps.has(packageName)) {
@@ -2116,13 +2123,17 @@ class LocalMiniappRuntime {
2116
2123
  });
2117
2124
  return;
2118
2125
  }
2119
- if (!isOverlayApp(packageName)) {
2126
+ if (!isDashboardApp(packageName)) {
2120
2127
  this.sendResult(packageName, requestId, false, undefined, {
2121
2128
  code: MiniappErrorCode.NOT_PERMITTED,
2122
- message: "The dashboard view belongs to system_dashboard apps",
2129
+ message: 'The dashboard view belongs to miniapps of type "dashboard"',
2123
2130
  });
2124
2131
  return;
2125
2132
  }
2133
+ if (!isOverlayApp(packageName)) {
2134
+ this.sendResult(packageName, requestId, true, { status: "unsupported", reason: "not the active dashboard" });
2135
+ return;
2136
+ }
2126
2137
  if (view === "main") {
2127
2138
  overlayPages.hide(packageName);
2128
2139
  this.sendResult(packageName, requestId, true, { status: "shown" });
@@ -2141,18 +2152,31 @@ class LocalMiniappRuntime {
2141
2152
  dashboardOpenTrace.slotGranted();
2142
2153
  this.sendResult(packageName, requestId, true, shown ? { status: "shown" } : { status: "unsupported", reason: "the overlay slot was refused" });
2143
2154
  }
2144
- // ---------------------------------------------------------------------------
2145
- // Settings bridge (session.settings) — system apps only
2146
- // ---------------------------------------------------------------------------
2147
- requireSystemForSettings(packageName, requestId) {
2148
- if (this.isSystemPackage(packageName))
2155
+ /**
2156
+ * Reject the request with PERMISSION_NOT_DECLARED unless the caller's
2157
+ * manifest declares `permission`. System apps pass without declaring it.
2158
+ */
2159
+ requirePermission(packageName, requestId, permission, context) {
2160
+ const caller = {
2161
+ system: this.isSystemPackage(packageName),
2162
+ permissions: this.connectedApps.get(packageName)?.installedManifest?.permissions,
2163
+ };
2164
+ if (mayUsePermission(caller, permission))
2149
2165
  return true;
2166
+ logPermissionNotDeclared(packageName, permission, context, `{"type": "${permission}"}`);
2150
2167
  this.sendResult(packageName, requestId, false, undefined, {
2151
- code: MiniappErrorCode.NOT_PERMITTED,
2152
- message: "The settings bridge is restricted to system apps",
2168
+ code: MiniappErrorCode.PERMISSION_NOT_DECLARED,
2169
+ message: `${permission} permission not declared in miniapp.json. Add {"type": "${permission}"} to the "permissions" array.`,
2170
+ permission,
2153
2171
  });
2154
2172
  return false;
2155
2173
  }
2174
+ // ---------------------------------------------------------------------------
2175
+ // Settings bridge (session.settings) — PLATFORM_SETTINGS permission
2176
+ // ---------------------------------------------------------------------------
2177
+ requireSettingsPermission(packageName, requestId) {
2178
+ return this.requirePermission(packageName, requestId, "PLATFORM_SETTINGS", "to use the settings bridge");
2179
+ }
2156
2180
  /** A bridged key's current value: the store's, or the engine's for a derived key. */
2157
2181
  settingsBridgeValue(key) {
2158
2182
  if (isDerivedSettingsKey(key))
@@ -2167,10 +2191,12 @@ class LocalMiniappRuntime {
2167
2191
  return localDisplayManager.foregroundAppOnScreen();
2168
2192
  case "ring_battery":
2169
2193
  return ringBatteryFrom(useGlassesStore.getState());
2194
+ case "active_dashboard":
2195
+ return activeDashboard();
2170
2196
  }
2171
2197
  }
2172
2198
  handleSettingsGet(packageName, payload, requestId) {
2173
- if (!this.requireSystemForSettings(packageName, requestId))
2199
+ if (!this.requireSettingsPermission(packageName, requestId))
2174
2200
  return;
2175
2201
  const values = {};
2176
2202
  for (const key of selectSettingsBridgeKeys(payload.keys))
@@ -2178,7 +2204,7 @@ class LocalMiniappRuntime {
2178
2204
  this.sendResult(packageName, requestId, true, { values });
2179
2205
  }
2180
2206
  async handleSettingsSet(packageName, payload, requestId) {
2181
- if (!this.requireSystemForSettings(packageName, requestId))
2207
+ if (!this.requireSettingsPermission(packageName, requestId))
2182
2208
  return;
2183
2209
  const validated = validateSettingsSet(payload);
2184
2210
  if (!validated.ok) {
@@ -2198,8 +2224,9 @@ class LocalMiniappRuntime {
2198
2224
  this.sendResult(packageName, requestId, true);
2199
2225
  }
2200
2226
  async handleNotificationAppsList(packageName, requestId) {
2201
- if (!this.requireSystemForSettings(packageName, requestId))
2227
+ if (!this.requirePermission(packageName, requestId, "READ_NOTIFICATIONS", "to list the apps that post notifications")) {
2202
2228
  return;
2229
+ }
2203
2230
  try {
2204
2231
  this.sendResult(packageName, requestId, true, await installedNotificationApps());
2205
2232
  }
@@ -2213,8 +2240,9 @@ class LocalMiniappRuntime {
2213
2240
  /**
2214
2241
  * Push bridged settings as they change: stored keys from the store, the
2215
2242
  * 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.
2243
+ * the apps store, the ring, the dashboard role). Subscribers declared
2244
+ * PLATFORM_SETTINGS (gated at SUBSCRIBE), so this is a no-op fan-out for
2245
+ * everyone else.
2218
2246
  */
2219
2247
  ensureSettingsBridgeWired() {
2220
2248
  if (this.settingsBridgeWired)
@@ -2249,6 +2277,7 @@ class LocalMiniappRuntime {
2249
2277
  onAppForegroundChange((foregrounded) => this.broadcastVisibility(foregrounded));
2250
2278
  localDisplayManager.onActiveDisplayChange(pushDerived);
2251
2279
  useAppStatusStore.subscribe(pushDerived);
2280
+ onOverlayAppsChange(pushDerived);
2252
2281
  useGlassesStore.subscribe((s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`, pushDerived);
2253
2282
  }
2254
2283
  /**
@@ -4562,6 +4591,33 @@ class LocalMiniappRuntime {
4562
4591
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.cfw_native_navigation.key), broadcast);
4563
4592
  // The dashboard master switch flips the dashboard view.
4564
4593
  useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.custom_dashboard_enabled.key), broadcast);
4594
+ // The wearer's choice and the installed dashboards decide who holds the role.
4595
+ const syncRole = () => this.syncActiveDashboard();
4596
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.active_dashboard.key), syncRole);
4597
+ useAppStatusStore.subscribe(syncRole);
4598
+ syncRole();
4599
+ }
4600
+ /**
4601
+ * Recompute who holds the dashboard role (ADR-0009). The previous holder
4602
+ * gives up the slot first, so it hears the view change while it is still
4603
+ * the overlay app; then every app sees its views again.
4604
+ */
4605
+ syncActiveDashboard() {
4606
+ const candidates = [...useAppStatusStore.getState().apps];
4607
+ // A dashboard can connect at launch before the apps store has listed it.
4608
+ for (const [packageName, app] of this.connectedApps) {
4609
+ if (candidates.some((candidate) => candidate.packageName === packageName))
4610
+ continue;
4611
+ candidates.push({ packageName, type: app.installedManifest?.type });
4612
+ }
4613
+ const next = activeDashboardAmong(candidates);
4614
+ const previous = activeDashboard();
4615
+ if (next === previous)
4616
+ return;
4617
+ if (previous !== null)
4618
+ overlayPages.hide(previous, "replaced");
4619
+ setActiveDashboard(next);
4620
+ this.broadcastCapabilities();
4565
4621
  }
4566
4622
  /** Capabilities as `packageName` sees them: the model profile plus the views this app may draw. */
4567
4623
  capabilitiesFor(packageName) {
@@ -4988,14 +5044,16 @@ class LocalMiniappRuntime {
4988
5044
  /* telemetry must never break an interop call */
4989
5045
  }
4990
5046
  }
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))
5047
+ /** Reject the request unless the caller may list, start and stop other miniapps (MANAGE_MINIAPPS). */
5048
+ requireMiniappsPermission(packageName, requestId, op, target) {
5049
+ if (this.requirePermission(packageName, requestId, "MANAGE_MINIAPPS", `to ${op} miniapps`))
4994
5050
  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",
5051
+ this.auditInterop({
5052
+ caller: packageName,
5053
+ op,
5054
+ target,
5055
+ ok: false,
5056
+ errorCode: MiniappErrorCode.PERMISSION_NOT_DECLARED,
4999
5057
  });
5000
5058
  return false;
5001
5059
  }
@@ -5018,7 +5076,7 @@ class LocalMiniappRuntime {
5018
5076
  };
5019
5077
  }
5020
5078
  handleMiniappsList(packageName, payload, requestId) {
5021
- if (!this.requireSystemCaller(packageName, requestId, "list"))
5079
+ if (!this.requireMiniappsPermission(packageName, requestId, "list"))
5022
5080
  return;
5023
5081
  const includeIncompatible = payload.includeIncompatible === true;
5024
5082
  const infos = this.interopApps()
@@ -5032,7 +5090,7 @@ class LocalMiniappRuntime {
5032
5090
  }
5033
5091
  async handleMiniappsStart(packageName, payload, requestId) {
5034
5092
  const target = payload.packageName;
5035
- if (!this.requireSystemCaller(packageName, requestId, "start", target))
5093
+ if (!this.requireMiniappsPermission(packageName, requestId, "start", target))
5036
5094
  return;
5037
5095
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined;
5038
5096
  if (!target || !app) {
@@ -5121,7 +5179,7 @@ class LocalMiniappRuntime {
5121
5179
  }
5122
5180
  async handleMiniappsStop(packageName, payload, requestId) {
5123
5181
  const target = payload.packageName;
5124
- if (!this.requireSystemCaller(packageName, requestId, "stop", target))
5182
+ if (!this.requireMiniappsPermission(packageName, requestId, "stop", target))
5125
5183
  return;
5126
5184
  if (!target) {
5127
5185
  this.sendResult(packageName, requestId, false, undefined, {