@glassly/engine 0.3.5-dev.745 → 0.3.5-dev.748

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 (42) hide show
  1. package/build/engine.d.ts +3 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +4 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/glasses.d.ts +5 -1
  6. package/build/facades/glasses.d.ts.map +1 -1
  7. package/build/facades/glasses.js +5 -0
  8. package/build/facades/glasses.js.map +1 -1
  9. package/build/generated/releaseMetadata.js +3 -3
  10. package/build/generated/releaseMetadata.js.map +1 -1
  11. package/build/services/LocalMiniappRuntime.d.ts +19 -4
  12. package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
  13. package/build/services/LocalMiniappRuntime.js +68 -125
  14. package/build/services/LocalMiniappRuntime.js.map +1 -1
  15. package/build/services/actionInvoke.d.ts +37 -0
  16. package/build/services/actionInvoke.d.ts.map +1 -0
  17. package/build/services/actionInvoke.js +89 -0
  18. package/build/services/actionInvoke.js.map +1 -0
  19. package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
  20. package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
  21. package/build/services/appleWatch/AppleWatchLink.js +259 -0
  22. package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
  23. package/build/services/controllers.d.ts.map +1 -1
  24. package/build/services/controllers.js +16 -0
  25. package/build/services/controllers.js.map +1 -1
  26. package/build/stores/settings.d.ts.map +1 -1
  27. package/build/stores/settings.js +9 -0
  28. package/build/stores/settings.js.map +1 -1
  29. package/build/types/enums.d.ts +2 -1
  30. package/build/types/enums.d.ts.map +1 -1
  31. package/build/types/enums.js +1 -0
  32. package/build/types/enums.js.map +1 -1
  33. package/package.json +7 -7
  34. package/src/engine.ts +4 -0
  35. package/src/facades/glasses.ts +7 -0
  36. package/src/generated/releaseMetadata.ts +3 -3
  37. package/src/services/LocalMiniappRuntime.ts +103 -125
  38. package/src/services/actionInvoke.ts +105 -0
  39. package/src/services/appleWatch/AppleWatchLink.ts +297 -0
  40. package/src/services/controllers.ts +17 -0
  41. package/src/stores/settings.ts +9 -0
  42. package/src/types/enums.ts +1 -0
@@ -12,9 +12,9 @@ export interface EngineReleaseMetadata {
12
12
  export const ENGINE_RELEASE_METADATA: Readonly<EngineReleaseMetadata> = Object.freeze({
13
13
  "schemaVersion": 1,
14
14
  "familyBaseVersion": "0.3.5",
15
- "releaseIdentity": "0.3.5-dev.745",
16
- "releaseSetId": "glassly-0.3.5-dev.745",
17
- "sourceCommit": "b6849b5ae18b0c0130e311eecde2b39c05f467f0",
15
+ "releaseIdentity": "0.3.5-dev.748",
16
+ "releaseSetId": "glassly-0.3.5-dev.748",
17
+ "sourceCommit": "101d04adfcd6f9273cee23325ab504a043f26eae",
18
18
  "otaManifestUrl": null,
19
19
  "otaManifestSha256": null
20
20
  })
@@ -162,6 +162,13 @@ import {invokeScanQrSeam} from "../runtime/scanQrSeam"
162
162
  import {invokeWebLoginSeam} from "../runtime/webLoginSeam"
163
163
  import {normalizeStreamAudioConfig, normalizeStreamVideoConfig} from "../runtime/streamConfig"
164
164
  import {visibleActionRows} from "./manifestActions"
165
+ import {
166
+ actionFailure,
167
+ appDeclaresAction,
168
+ checkActionAccess,
169
+ PendingActionCalls,
170
+ type ActionOutcome,
171
+ } from "./actionInvoke"
165
172
  import {toLanguageHint} from "@glassly/cloud-protocol/languages"
166
173
  import {buildMiniappManifestSnapshot, type MiniappRuntimeDiagnosticSnapshot} from "../utils/miniappDiagnostics"
167
174
  import ttsModelManager from "./TTSModelManager"
@@ -6021,10 +6028,7 @@ class LocalMiniappRuntime {
6021
6028
  private static readonly ACTION_PAYLOAD_CAP = 256 * 1024
6022
6029
 
6023
6030
  /** Outstanding action invocations, keyed by host-generated callId. */
6024
- private actionCalls = new Map<
6025
- string,
6026
- {callerPackageName: string; targetPackageName: string; callerRequestId?: string; timer: number}
6027
- >()
6031
+ private actionCalls = new PendingActionCalls(BgTimer, (value) => this.actionPayloadTooLarge(value))
6028
6032
  private actionCallSeq = 0
6029
6033
 
6030
6034
  private interopApps(): ClientApp[] {
@@ -6596,111 +6600,118 @@ class LocalMiniappRuntime {
6596
6600
  }
6597
6601
  }
6598
6602
 
6603
+ /** Floored at 6s: a freshly-woken target buffers an undelivered ACTION_CALL for 5s (HANDLER_WAIT_MS in @glassly/miniapp's actions module) + 1s for the reply. Keep in sync. */
6604
+ private static clampActionTimeout(raw: unknown): number {
6605
+ return Math.min(Math.max(Number(raw) || 30_000, 6_000), 120_000)
6606
+ }
6607
+
6608
+ /** Caller identity the host presents for host-initiated actions (a system caller). */
6609
+ private static readonly HOST_ACTION_CALLER = "host"
6610
+
6611
+ /** Installed, compatible, and declares the action — the same gates `invokeActionFromHost` applies up front. */
6612
+ public hostActionAvailable(targetPackageName: string, actionId: string): boolean {
6613
+ return appDeclaresAction(
6614
+ this.interopApps().find((a) => a.packageName === targetPackageName),
6615
+ actionId,
6616
+ )
6617
+ }
6618
+
6599
6619
  /**
6600
- * session.actions.invoke → wake the target headlessly, deliver an ACTION_CALL,
6601
- * and correlate its ACTION_RESULT back to the caller's pending request.
6620
+ * Host-initiated action call (e.g. the watch remote): same gates, headless
6621
+ * wake and ACTION_CALL/ACTION_RESULT correlation as a miniapp's
6622
+ * session.actions.invoke, with the host as a system caller.
6623
+ * Rejects with an Error whose `code` is a MiniappErrorCode.
6602
6624
  */
6625
+ public async invokeActionFromHost(
6626
+ targetPackageName: string,
6627
+ actionId: string,
6628
+ params: Record<string, unknown> = {},
6629
+ timeoutMs?: number,
6630
+ ): Promise<unknown> {
6631
+ const outcome = await this.runActionInvoke(
6632
+ LocalMiniappRuntime.HOST_ACTION_CALLER,
6633
+ true,
6634
+ targetPackageName,
6635
+ actionId,
6636
+ params,
6637
+ LocalMiniappRuntime.clampActionTimeout(timeoutMs),
6638
+ )
6639
+ if (outcome.ok) return outcome.data
6640
+ throw Object.assign(new Error(outcome.error.message), {code: outcome.error.code})
6641
+ }
6642
+
6643
+ /** session.actions.invoke → run the call, then relay the outcome to the caller's pending request. */
6603
6644
  private async handleActionInvoke(
6604
6645
  callerPackageName: string,
6605
6646
  payload: Record<string, unknown>,
6606
6647
  requestId?: string,
6607
6648
  ): Promise<void> {
6608
- const target = payload.targetPackageName as string | undefined
6609
- const actionId = payload.actionId as string | undefined
6610
- const params = (payload.params as Record<string, unknown> | undefined) ?? {}
6611
- // Floor the timeout at 6s so the host never rejects with ACTION_TIMEOUT before
6612
- // the target SDK's handler-registration window closes. A freshly-woken miniapp
6613
- // buffers an undelivered ACTION_CALL for HANDLER_WAIT_MS (5s in @glassly/miniapp's
6614
- // actions module) waiting for session.actions.handle; a shorter host timeout
6615
- // could fire while the target is still registering, then the action would run
6616
- // with no caller left to receive its result. 6s = that 5s buffer + 1s for the
6617
- // ACTION_RESULT/NO_ACTION_HANDLER reply to travel back. Keep these in sync.
6618
- const timeoutMs = Math.min(Math.max(Number(payload.timeoutMs) || 30_000, 6_000), 120_000)
6649
+ const outcome = await this.runActionInvoke(
6650
+ callerPackageName,
6651
+ this.isSystemPackage(callerPackageName),
6652
+ payload.targetPackageName as string | undefined,
6653
+ payload.actionId as string | undefined,
6654
+ (payload.params as Record<string, unknown> | undefined) ?? {},
6655
+ LocalMiniappRuntime.clampActionTimeout(payload.timeoutMs),
6656
+ )
6657
+ if (outcome.ok) this.sendResult(callerPackageName, requestId, true, outcome.data)
6658
+ else this.sendResult(callerPackageName, requestId, false, undefined, outcome.error)
6659
+ }
6660
+
6661
+ /**
6662
+ * Gate, wake the target headlessly, deliver an ACTION_CALL, and resolve when
6663
+ * its ACTION_RESULT (or the timeout) arrives. Never rejects.
6664
+ */
6665
+ private async runActionInvoke(
6666
+ callerPackageName: string,
6667
+ callerIsSystem: boolean,
6668
+ target: string | undefined,
6669
+ actionId: string | undefined,
6670
+ params: Record<string, unknown>,
6671
+ timeoutMs: number,
6672
+ ): Promise<ActionOutcome> {
6673
+ const fail = actionFailure
6619
6674
 
6620
6675
  if (this.actionPayloadTooLarge(params)) {
6621
- this.sendResult(callerPackageName, requestId, false, undefined, {
6622
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
6623
- message: "action params exceeded the 256 KB cap",
6624
- })
6625
- return
6676
+ return fail(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action params exceeded the 256 KB cap")
6626
6677
  }
6627
6678
 
6628
6679
  const app = target ? this.interopApps().find((a) => a.packageName === target) : undefined
6629
- if (!target || !app) {
6630
- this.sendResult(callerPackageName, requestId, false, undefined, {
6631
- code: MiniappErrorCode.APP_NOT_FOUND,
6632
- message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
6633
- })
6634
- return
6635
- }
6636
- if (!actionId) {
6637
- this.sendResult(callerPackageName, requestId, false, undefined, {
6638
- code: MiniappErrorCode.ACTION_NOT_FOUND,
6639
- message: "invoke requires an actionId",
6640
- })
6641
- return
6642
- }
6643
- if (app.compatibility && app.compatibility.isCompatible === false) {
6644
- this.sendResult(callerPackageName, requestId, false, undefined, {
6645
- code: MiniappErrorCode.APP_NOT_COMPATIBLE,
6646
- message: `${target} is not compatible with the connected glasses`,
6647
- })
6648
- islandNotifications.emit({
6649
- kind: "version_incompatible",
6650
- packageName: target,
6651
- reason: `${target} is not compatible with the connected glasses`,
6652
- metadata: {missingRequired: app.compatibility.missingRequired ?? [], via: "miniapps.invoke"},
6653
- timestamp: Date.now(),
6654
- })
6655
- return
6656
- }
6657
- // Declared-action gate: the target must declare the action in its manifest.
6658
- const declared = (app.actions ?? []).find((a) => a.id === actionId)
6659
- if (!declared) {
6660
- this.sendResult(callerPackageName, requestId, false, undefined, {
6661
- code: MiniappErrorCode.ACTION_NOT_FOUND,
6662
- message: `${target} does not declare action "${actionId}"`,
6663
- })
6664
- return
6665
- }
6666
- // Visibility gate: declared actions are public by default; ones marked
6667
- // `visibility: "system"` stay restricted to system/dev callers.
6668
- if (declared.visibility === "system" && !this.isSystemPackage(callerPackageName)) {
6669
- this.auditInterop({
6670
- caller: callerPackageName,
6671
- op: "invoke",
6672
- target,
6673
- actionId,
6674
- ok: false,
6675
- errorCode: MiniappErrorCode.NOT_PERMITTED,
6676
- })
6677
- this.sendResult(callerPackageName, requestId, false, undefined, {
6678
- code: MiniappErrorCode.NOT_PERMITTED,
6679
- message: `Action "${actionId}" on ${target} is restricted to system apps`,
6680
- })
6681
- return
6680
+ const denied = checkActionAccess(app, target, actionId, callerIsSystem)
6681
+ if (denied) {
6682
+ if (denied.code === MiniappErrorCode.APP_NOT_COMPATIBLE && target && app) {
6683
+ islandNotifications.emit({
6684
+ kind: "version_incompatible",
6685
+ packageName: target,
6686
+ reason: denied.message,
6687
+ metadata: {missingRequired: app.compatibility?.missingRequired ?? [], via: "miniapps.invoke"},
6688
+ timestamp: Date.now(),
6689
+ })
6690
+ } else if (denied.code === MiniappErrorCode.NOT_PERMITTED) {
6691
+ this.auditInterop({
6692
+ caller: callerPackageName,
6693
+ op: "invoke",
6694
+ target,
6695
+ actionId,
6696
+ ok: false,
6697
+ errorCode: MiniappErrorCode.NOT_PERMITTED,
6698
+ })
6699
+ }
6700
+ return {ok: false, error: denied}
6682
6701
  }
6702
+ if (!target || !actionId) return fail(MiniappErrorCode.INTERNAL, "unreachable")
6683
6703
 
6684
6704
  // Headless wake + wait for CONNECT (idempotent / fast if already connected).
6685
6705
  try {
6686
6706
  await this.wakeInteropApp(target)
6687
6707
  } catch (e) {
6688
- this.sendResult(callerPackageName, requestId, false, undefined, {
6689
- code: MiniappErrorCode.WAKE_FAILED,
6690
- message: (e as Error)?.message ?? `failed to wake ${target}`,
6691
- })
6692
- return
6708
+ return fail(MiniappErrorCode.WAKE_FAILED, (e as Error)?.message ?? `failed to wake ${target}`)
6693
6709
  }
6694
6710
 
6695
6711
  // The wake waited for CONNECT, but the target could have dropped in the gap
6696
6712
  // before delivery — fail fast rather than arming a timer for a call that
6697
- // sendToMiniapp would silently drop (the caller would otherwise wait the
6698
- // full invoke timeout for an ACTION_CALL that was never delivered).
6713
+ // sendToMiniapp would silently drop.
6699
6714
  if (!this.connectedApps.has(target)) {
6700
- this.sendResult(callerPackageName, requestId, false, undefined, {
6701
- code: MiniappErrorCode.WAKE_FAILED,
6702
- message: `${target} disconnected before the action could be delivered`,
6703
- })
6704
6715
  this.auditInterop({
6705
6716
  caller: callerPackageName,
6706
6717
  op: "invoke",
@@ -6709,19 +6720,12 @@ class LocalMiniappRuntime {
6709
6720
  ok: false,
6710
6721
  errorCode: MiniappErrorCode.WAKE_FAILED,
6711
6722
  })
6712
- return
6723
+ return fail(MiniappErrorCode.WAKE_FAILED, `${target} disconnected before the action could be delivered`)
6713
6724
  }
6714
6725
 
6715
- // Deliver the call and arm the handler timeout; ACTION_RESULT resolves it.
6726
+ // Deliver the call and arm the handler timeout; ACTION_RESULT settles it.
6716
6727
  const callId = `act-${Date.now().toString(36)}-${this.actionCallSeq++}`
6717
- const timer = BgTimer.setTimeout(() => {
6718
- this.actionCalls.delete(callId)
6719
- this.sendResult(callerPackageName, requestId, false, undefined, {
6720
- code: MiniappErrorCode.ACTION_TIMEOUT,
6721
- message: `action "${actionId}" timed out after ${timeoutMs}ms`,
6722
- })
6723
- }, timeoutMs)
6724
- this.actionCalls.set(callId, {callerPackageName, targetPackageName: target, callerRequestId: requestId, timer})
6728
+ const outcome = this.actionCalls.register(callId, target, actionId, timeoutMs)
6725
6729
  this.sendToMiniapp(target, {
6726
6730
  type: MiniappResponseType.ACTION_CALL,
6727
6731
  callId,
@@ -6730,38 +6734,12 @@ class LocalMiniappRuntime {
6730
6734
  callerPackageName,
6731
6735
  })
6732
6736
  this.auditInterop({caller: callerPackageName, op: "invoke", target, actionId, ok: true})
6737
+ return outcome
6733
6738
  }
6734
6739
 
6735
- /** Target → host: forward an ACTION_RESULT back to the caller's pending invoke. */
6740
+ /** Target → host: settle the pending invoke for this ACTION_RESULT. */
6736
6741
  private handleActionResult(targetPackageName: string, payload: Record<string, unknown>): void {
6737
- const callId = payload.callId as string | undefined
6738
- if (!callId) return
6739
- const pending = this.actionCalls.get(callId)
6740
- if (!pending) return
6741
- // Only the invoked target may resolve this call. callIds are guessable
6742
- // (timestamp + seq), so a different connected miniapp must not be able to
6743
- // spoof a result/error for someone else's invoke.
6744
- if (pending.targetPackageName !== targetPackageName) return
6745
- this.actionCalls.delete(callId)
6746
- BgTimer.clearTimeout(pending.timer)
6747
-
6748
- if (payload.ok === true) {
6749
- const result = payload.result
6750
- if (this.actionPayloadTooLarge(result)) {
6751
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, {
6752
- code: MiniappErrorCode.PAYLOAD_TOO_LARGE,
6753
- message: "action result exceeded the 256 KB cap",
6754
- })
6755
- return
6756
- }
6757
- this.sendResult(pending.callerPackageName, pending.callerRequestId, true, result ?? null)
6758
- } else {
6759
- const error = (payload.error as ({code: string; message: string} & Record<string, unknown>) | undefined) ?? {
6760
- code: MiniappErrorCode.INTERNAL,
6761
- message: "action handler error",
6762
- }
6763
- this.sendResult(pending.callerPackageName, pending.callerRequestId, false, undefined, error)
6764
- }
6742
+ this.actionCalls.handleResult(targetPackageName, payload)
6765
6743
  }
6766
6744
 
6767
6745
  private sendResult(
@@ -0,0 +1,105 @@
1
+ import {MiniappErrorCode} from "@glassly/miniapp"
2
+
3
+ import type {ClientApp} from "../types/applet"
4
+
5
+ export type ActionError = {code: string; message: string} & Record<string, unknown>
6
+ export type ActionOutcome = {ok: true; data: unknown} | {ok: false; error: ActionError}
7
+
8
+ export const actionFailure = (code: string, message: string): ActionOutcome => ({ok: false, error: {code, message}})
9
+
10
+ /** Installed, compatible, and declares the action. */
11
+ export function appDeclaresAction(app: ClientApp | undefined, actionId: string): boolean {
12
+ if (!app) return false
13
+ if (app.compatibility && app.compatibility.isCompatible === false) return false
14
+ return (app.actions ?? []).some((a) => a.id === actionId)
15
+ }
16
+
17
+ /**
18
+ * Gates shared by miniapp->miniapp invoke and host invoke. Declared actions are
19
+ * public by default; `visibility: "system"` ones need a system caller.
20
+ */
21
+ export function checkActionAccess(
22
+ app: ClientApp | undefined,
23
+ target: string | undefined,
24
+ actionId: string | undefined,
25
+ callerIsSystem: boolean,
26
+ ): ActionError | null {
27
+ if (!target || !app) {
28
+ return {
29
+ code: MiniappErrorCode.APP_NOT_FOUND,
30
+ message: `Miniapp not found: ${target ?? "(missing targetPackageName)"}`,
31
+ }
32
+ }
33
+ if (!actionId) return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: "invoke requires an actionId"}
34
+ if (app.compatibility && app.compatibility.isCompatible === false) {
35
+ return {
36
+ code: MiniappErrorCode.APP_NOT_COMPATIBLE,
37
+ message: `${target} is not compatible with the connected glasses`,
38
+ }
39
+ }
40
+ const declared = (app.actions ?? []).find((a) => a.id === actionId)
41
+ if (!declared) {
42
+ return {code: MiniappErrorCode.ACTION_NOT_FOUND, message: `${target} does not declare action "${actionId}"`}
43
+ }
44
+ if (declared.visibility === "system" && !callerIsSystem) {
45
+ return {
46
+ code: MiniappErrorCode.NOT_PERMITTED,
47
+ message: `Action "${actionId}" on ${target} is restricted to system apps`,
48
+ }
49
+ }
50
+ return null
51
+ }
52
+
53
+ /** BgTimer's shape; passed in so this module stays loadable without the native timer. */
54
+ export interface ActionTimers {
55
+ setTimeout: (cb: () => void, ms: number) => number
56
+ clearTimeout: (id: number) => void
57
+ }
58
+
59
+ /** Outstanding ACTION_CALLs awaiting an ACTION_RESULT, keyed by host-generated callId. */
60
+ export class PendingActionCalls {
61
+ private readonly calls = new Map<string, {target: string; settle: (o: ActionOutcome) => void; timer: number}>()
62
+
63
+ constructor(
64
+ private readonly timers: ActionTimers,
65
+ private readonly payloadTooLarge: (value: unknown) => boolean,
66
+ ) {}
67
+
68
+ /** Register a call; resolves with its outcome, or ACTION_TIMEOUT. */
69
+ register(callId: string, target: string, actionId: string, timeoutMs: number): Promise<ActionOutcome> {
70
+ return new Promise((resolve) => {
71
+ const timer = this.timers.setTimeout(() => {
72
+ this.calls.delete(callId)
73
+ resolve(actionFailure(MiniappErrorCode.ACTION_TIMEOUT, `action "${actionId}" timed out after ${timeoutMs}ms`))
74
+ }, timeoutMs)
75
+ this.calls.set(callId, {target, settle: resolve, timer})
76
+ })
77
+ }
78
+
79
+ /** Settle from a target's ACTION_RESULT payload. Only the invoked target may resolve a call. */
80
+ handleResult(targetPackageName: string, payload: Record<string, unknown>): void {
81
+ const callId = payload.callId as string | undefined
82
+ if (!callId) return
83
+ const pending = this.calls.get(callId)
84
+ // callIds are guessable (timestamp + seq): a different miniapp must not spoof someone else's result.
85
+ if (!pending || pending.target !== targetPackageName) return
86
+ this.calls.delete(callId)
87
+ this.timers.clearTimeout(pending.timer)
88
+
89
+ if (payload.ok === true) {
90
+ if (this.payloadTooLarge(payload.result)) {
91
+ pending.settle(actionFailure(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action result exceeded the 256 KB cap"))
92
+ return
93
+ }
94
+ pending.settle({ok: true, data: payload.result ?? null})
95
+ return
96
+ }
97
+ pending.settle({
98
+ ok: false,
99
+ error: (payload.error as ActionError | undefined) ?? {
100
+ code: MiniappErrorCode.INTERNAL,
101
+ message: "action handler error",
102
+ },
103
+ })
104
+ }
105
+ }