@glassly/engine 0.3.5-dev.743 → 0.3.5-dev.746
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/engine.d.ts +3 -0
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +4 -0
- package/build/engine.js.map +1 -1
- package/build/facades/glasses.d.ts +5 -1
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +5 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +16 -2
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +19 -4
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +68 -125
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/actionInvoke.d.ts +36 -0
- package/build/services/actionInvoke.d.ts.map +1 -0
- package/build/services/actionInvoke.js +89 -0
- package/build/services/actionInvoke.js.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts +42 -0
- package/build/services/appleWatch/AppleWatchLink.d.ts.map +1 -0
- package/build/services/appleWatch/AppleWatchLink.js +259 -0
- package/build/services/appleWatch/AppleWatchLink.js.map +1 -0
- package/build/services/controllers.d.ts.map +1 -1
- package/build/services/controllers.js +16 -0
- package/build/services/controllers.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +9 -0
- package/build/stores/settings.js.map +1 -1
- package/build/types/enums.d.ts +2 -1
- package/build/types/enums.d.ts.map +1 -1
- package/build/types/enums.js +1 -0
- package/build/types/enums.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +4 -0
- package/src/facades/glasses.ts +7 -0
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/services/GlassesTouchInput.ts +16 -2
- package/src/services/LocalMiniappRuntime.ts +103 -125
- package/src/services/actionInvoke.ts +104 -0
- package/src/services/appleWatch/AppleWatchLink.ts +297 -0
- package/src/services/controllers.ts +17 -0
- package/src/stores/settings.ts +9 -0
- 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.
|
|
16
|
-
"releaseSetId": "glassly-0.3.5-dev.
|
|
17
|
-
"sourceCommit": "
|
|
15
|
+
"releaseIdentity": "0.3.5-dev.746",
|
|
16
|
+
"releaseSetId": "glassly-0.3.5-dev.746",
|
|
17
|
+
"sourceCommit": "cac52fa70073682f25120d73f6485c77c4117efe",
|
|
18
18
|
"otaManifestUrl": null,
|
|
19
19
|
"otaManifestSha256": null
|
|
20
20
|
})
|
|
@@ -5,6 +5,7 @@ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
|
|
|
5
5
|
import {useAppStatusStore} from "../stores/apps"
|
|
6
6
|
import {useGlassesStore} from "../stores/glasses"
|
|
7
7
|
import {BgTimer} from "../utils/timers"
|
|
8
|
+
import {hasInput2d} from "./controllers"
|
|
8
9
|
import dashboardOpenTrace from "./debug/DashboardOpenTrace"
|
|
9
10
|
import gestureDotsService from "./GestureDotsService"
|
|
10
11
|
import {isGlassesConnected} from "./GlassesReadiness"
|
|
@@ -43,6 +44,19 @@ function shouldForwardScroll(event: TouchEvent): boolean {
|
|
|
43
44
|
return true
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Miniapps' copy of the touch: `input2d: true` when it came from a controller
|
|
49
|
+
* that swipes in two dimensions (the Neural Band), the field absent otherwise,
|
|
50
|
+
* so a 1D swipe from the temples or the ring stays tellable while a band is
|
|
51
|
+
* connected under the same gesture names.
|
|
52
|
+
*/
|
|
53
|
+
function toMiniapps(event: TouchEvent): TouchEvent & {input2d?: true} {
|
|
54
|
+
const id = event.controllerId
|
|
55
|
+
if (id === undefined) return event
|
|
56
|
+
const model = useGlassesStore.getState().controllers?.[id]?.model
|
|
57
|
+
return model !== undefined && hasInput2d(model) ? {...event, input2d: true} : event
|
|
58
|
+
}
|
|
59
|
+
|
|
46
60
|
/**
|
|
47
61
|
* Route both real glasses input and phone mirror controls through the same
|
|
48
62
|
* policy. A claimed gesture goes to the app holding the claim, alone; an
|
|
@@ -88,7 +102,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
88
102
|
// The press may have started before this app claimed its release edge.
|
|
89
103
|
gestureDotsService.noteGesture(gesture, true, event.index, event.controllerId)
|
|
90
104
|
if (!shouldForwardScroll(event)) return
|
|
91
|
-
localMiniappRuntime.forwardEvent("touch_event", event, undefined, claimant)
|
|
105
|
+
localMiniappRuntime.forwardEvent("touch_event", toMiniapps(event), undefined, claimant)
|
|
92
106
|
return
|
|
93
107
|
}
|
|
94
108
|
|
|
@@ -98,7 +112,7 @@ export function dispatchTouchEvent(event: TouchEvent): void {
|
|
|
98
112
|
gestureDotsService.noteGesture(gesture, false, event.index, event.controllerId)
|
|
99
113
|
|
|
100
114
|
if (!shouldForwardScroll(event)) return
|
|
101
|
-
localMiniappRuntime.forwardEvent("touch_event", event)
|
|
115
|
+
localMiniappRuntime.forwardEvent("touch_event", toMiniapps(event))
|
|
102
116
|
|
|
103
117
|
// Nobody claimed a double tap or a head-up: the glasses' native dashboard.
|
|
104
118
|
// On eligible Glassly CFW glasses the dashboard miniapp claims both while it
|
|
@@ -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
|
|
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
|
-
*
|
|
6601
|
-
* and
|
|
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
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
|
|
6633
|
-
|
|
6634
|
-
|
|
6635
|
-
|
|
6636
|
-
|
|
6637
|
-
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
6641
|
-
|
|
6642
|
-
|
|
6643
|
-
|
|
6644
|
-
|
|
6645
|
-
|
|
6646
|
-
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
6740
|
+
/** Target → host: settle the pending invoke for this ACTION_RESULT. */
|
|
6736
6741
|
private handleActionResult(targetPackageName: string, payload: Record<string, unknown>): void {
|
|
6737
|
-
|
|
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,104 @@
|
|
|
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
|
+
export interface ActionTimers {
|
|
54
|
+
setTimeout(cb: () => void, ms: number): number
|
|
55
|
+
clearTimeout(id: number): void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Outstanding ACTION_CALLs awaiting an ACTION_RESULT, keyed by host-generated callId. */
|
|
59
|
+
export class PendingActionCalls {
|
|
60
|
+
private readonly calls = new Map<string, {target: string; settle: (o: ActionOutcome) => void; timer: number}>()
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly timers: ActionTimers,
|
|
64
|
+
private readonly payloadTooLarge: (value: unknown) => boolean,
|
|
65
|
+
) {}
|
|
66
|
+
|
|
67
|
+
/** Register a call; resolves with its outcome, or ACTION_TIMEOUT. */
|
|
68
|
+
register(callId: string, target: string, actionId: string, timeoutMs: number): Promise<ActionOutcome> {
|
|
69
|
+
return new Promise((resolve) => {
|
|
70
|
+
const timer = this.timers.setTimeout(() => {
|
|
71
|
+
this.calls.delete(callId)
|
|
72
|
+
resolve(actionFailure(MiniappErrorCode.ACTION_TIMEOUT, `action "${actionId}" timed out after ${timeoutMs}ms`))
|
|
73
|
+
}, timeoutMs)
|
|
74
|
+
this.calls.set(callId, {target, settle: resolve, timer})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Settle from a target's ACTION_RESULT payload. Only the invoked target may resolve a call. */
|
|
79
|
+
handleResult(targetPackageName: string, payload: Record<string, unknown>): void {
|
|
80
|
+
const callId = payload.callId as string | undefined
|
|
81
|
+
if (!callId) return
|
|
82
|
+
const pending = this.calls.get(callId)
|
|
83
|
+
// callIds are guessable (timestamp + seq): a different miniapp must not spoof someone else's result.
|
|
84
|
+
if (!pending || pending.target !== targetPackageName) return
|
|
85
|
+
this.calls.delete(callId)
|
|
86
|
+
this.timers.clearTimeout(pending.timer)
|
|
87
|
+
|
|
88
|
+
if (payload.ok === true) {
|
|
89
|
+
if (this.payloadTooLarge(payload.result)) {
|
|
90
|
+
pending.settle(actionFailure(MiniappErrorCode.PAYLOAD_TOO_LARGE, "action result exceeded the 256 KB cap"))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
pending.settle({ok: true, data: payload.result ?? null})
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
pending.settle({
|
|
97
|
+
ok: false,
|
|
98
|
+
error: (payload.error as ActionError | undefined) ?? {
|
|
99
|
+
code: MiniappErrorCode.INTERNAL,
|
|
100
|
+
message: "action handler error",
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|