@glassly/engine 0.3.2-beta.572 → 0.3.2-beta.602
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/README.md +1 -1
- package/build/engine.d.ts +15 -1
- package/build/engine.d.ts.map +1 -1
- package/build/engine.js +7 -3
- package/build/engine.js.map +1 -1
- package/build/facades/dev.d.ts +2 -2
- package/build/facades/dev.d.ts.map +1 -1
- package/build/facades/dev.js +2 -2
- package/build/facades/dev.js.map +1 -1
- package/build/facades/glasses.d.ts +20 -0
- package/build/facades/glasses.d.ts.map +1 -1
- package/build/facades/glasses.js +21 -0
- package/build/facades/glasses.js.map +1 -1
- package/build/facades/pairing.js +1 -1
- package/build/facades/pairing.js.map +1 -1
- package/build/facades/reports.js +2 -2
- package/build/facades/reports.js.map +1 -1
- package/build/facades/session.d.ts +1 -1
- package/build/facades/session.js +2 -2
- package/build/facades/session.js.map +1 -1
- package/build/generated/releaseMetadata.js +3 -3
- package/build/generated/releaseMetadata.js.map +1 -1
- package/build/index.d.ts +3 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +3 -1
- package/build/index.js.map +1 -1
- package/build/runtime/bootstrap.d.ts +2 -2
- package/build/runtime/bootstrap.d.ts.map +1 -1
- package/build/runtime/bootstrap.js.map +1 -1
- package/build/services/CloudAudioSubscriptionSync.d.ts +2 -2
- package/build/services/CloudAudioSubscriptionSync.js +2 -2
- package/build/services/CloudAudioSubscriptionSync.js.map +1 -1
- package/build/services/CloudClientService.d.ts +2 -2
- package/build/services/CloudClientService.d.ts.map +1 -1
- package/build/services/CloudClientService.js +4 -4
- package/build/services/CloudClientService.js.map +1 -1
- package/build/services/CloudTranscriptE2EMetrics.d.ts +2 -2
- package/build/services/CloudTranscriptE2EMetrics.d.ts.map +1 -1
- package/build/services/CloudTranscriptE2EMetrics.js +3 -3
- package/build/services/CloudTranscriptE2EMetrics.js.map +1 -1
- package/build/services/DeviceEventRouter.d.ts.map +1 -1
- package/build/services/DeviceEventRouter.js +5 -0
- package/build/services/DeviceEventRouter.js.map +1 -1
- package/build/services/GestureDotsService.d.ts +9 -9
- package/build/services/GestureDotsService.d.ts.map +1 -1
- package/build/services/GestureDotsService.js +75 -50
- package/build/services/GestureDotsService.js.map +1 -1
- package/build/services/GlassesTouchInput.d.ts +1 -1
- package/build/services/GlassesTouchInput.d.ts.map +1 -1
- package/build/services/GlassesTouchInput.js +7 -4
- package/build/services/GlassesTouchInput.js.map +1 -1
- package/build/services/LocalMiniappRuntime.d.ts +1 -1
- package/build/services/LocalMiniappRuntime.d.ts.map +1 -1
- package/build/services/LocalMiniappRuntime.js +32 -8
- package/build/services/LocalMiniappRuntime.js.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.d.ts +5 -2
- package/build/services/LocalSttFallbackCoordinator.d.ts.map +1 -1
- package/build/services/LocalSttFallbackCoordinator.js +7 -3
- package/build/services/LocalSttFallbackCoordinator.js.map +1 -1
- package/build/services/PhonePhotoCoordinator.d.ts +1 -1
- package/build/services/PhonePhotoCoordinator.js +4 -4
- package/build/services/PhonePhotoCoordinator.js.map +1 -1
- package/build/services/PhoneStreamCoordinator.d.ts +1 -1
- package/build/services/PhoneStreamCoordinator.js +1 -1
- package/build/services/PhoneStreamCoordinator.js.map +1 -1
- package/build/services/SupportProfileSync.d.ts +1 -1
- package/build/services/SupportProfileSync.js +2 -2
- package/build/services/SupportProfileSync.js.map +1 -1
- package/build/services/TranscriptionRouting.d.ts +6 -1
- package/build/services/TranscriptionRouting.d.ts.map +1 -1
- package/build/services/TranscriptionRouting.js +7 -3
- package/build/services/TranscriptionRouting.js.map +1 -1
- package/build/services/cloudStreamApi.d.ts +2 -2
- package/build/services/cloudStreamApi.js +2 -2
- package/build/services/cloudStreamApi.js.map +1 -1
- package/build/services/gestureDotGlyphs.d.ts +1 -1
- package/build/services/gestureDotGlyphs.d.ts.map +1 -1
- package/build/services/gestureDotGlyphs.js +5 -0
- package/build/services/gestureDotGlyphs.js.map +1 -1
- package/build/services/neuralBand/MetaAuth.d.ts +31 -0
- package/build/services/neuralBand/MetaAuth.d.ts.map +1 -0
- package/build/services/neuralBand/MetaAuth.js +161 -0
- package/build/services/neuralBand/MetaAuth.js.map +1 -0
- package/build/services/neuralBand/NeuralBandInput.d.ts +50 -0
- package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -0
- package/build/services/neuralBand/NeuralBandInput.js +114 -0
- package/build/services/neuralBand/NeuralBandInput.js.map +1 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.d.ts +3 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.js +52 -0
- package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -0
- package/build/services/neuralBand/sha256.d.ts +7 -0
- package/build/services/neuralBand/sha256.d.ts.map +1 -0
- package/build/services/neuralBand/sha256.js +68 -0
- package/build/services/neuralBand/sha256.js.map +1 -0
- package/build/services/settingsBridge.d.ts +12 -4
- package/build/services/settingsBridge.d.ts.map +1 -1
- package/build/services/settingsBridge.js +13 -4
- package/build/services/settingsBridge.js.map +1 -1
- package/build/services/soniox/SonioxRealtimeClient.d.ts +2 -2
- package/build/services/soniox/SonioxRealtimeClient.js +2 -2
- package/build/services/soniox/SonioxRealtimeClient.js.map +1 -1
- package/build/services/soniox/SonioxTokenAssembler.d.ts +1 -1
- package/build/services/soniox/SonioxTokenAssembler.js +1 -1
- package/build/services/soniox/SonioxTokenAssembler.js.map +1 -1
- package/build/stores/cloudClientStatus.js +1 -1
- package/build/stores/cloudClientStatus.js.map +1 -1
- package/build/stores/settings.d.ts.map +1 -1
- package/build/stores/settings.js +25 -7
- 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/build/utils/cloudClient/cloudSecureStore.js +2 -2
- package/build/utils/cloudClient/cloudSecureStore.js.map +1 -1
- package/package.json +7 -7
- package/src/engine.ts +7 -3
- package/src/facades/dev.ts +2 -2
- package/src/facades/glasses.ts +27 -0
- package/src/facades/pairing.ts +1 -1
- package/src/facades/reports.ts +2 -2
- package/src/facades/session.ts +2 -2
- package/src/generated/releaseMetadata.ts +3 -3
- package/src/index.ts +15 -1
- package/src/runtime/bootstrap.ts +2 -2
- package/src/services/CloudAudioSubscriptionSync.ts +2 -2
- package/src/services/CloudClientService.ts +4 -4
- package/src/services/CloudTranscriptE2EMetrics.ts +3 -3
- package/src/services/DeviceEventRouter.ts +5 -0
- package/src/services/GestureDotsService.ts +72 -49
- package/src/services/GlassesTouchInput.ts +13 -5
- package/src/services/LocalMiniappRuntime.ts +52 -24
- package/src/services/LocalSttFallbackCoordinator.ts +18 -4
- package/src/services/PhonePhotoCoordinator.ts +4 -4
- package/src/services/PhoneStreamCoordinator.ts +1 -1
- package/src/services/SupportProfileSync.ts +2 -2
- package/src/services/TranscriptionRouting.ts +12 -4
- package/src/services/cloudStreamApi.ts +3 -3
- package/src/services/gestureDotGlyphs.ts +6 -1
- package/src/services/neuralBand/MetaAuth.ts +192 -0
- package/src/services/neuralBand/NeuralBandInput.ts +148 -0
- package/src/services/neuralBand/NeuralBandSettingsSync.ts +63 -0
- package/src/services/neuralBand/sha256.ts +70 -0
- package/src/services/settingsBridge.ts +15 -4
- package/src/services/soniox/SonioxRealtimeClient.ts +2 -2
- package/src/services/soniox/SonioxTokenAssembler.ts +1 -1
- package/src/stores/cloudClientStatus.ts +1 -1
- package/src/stores/settings.ts +25 -7
- package/src/types/enums.ts +1 -0
- package/src/utils/cloudClient/cloudSecureStore.ts +2 -2
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
import {normalizeAiTier, resolveAiTier} from "./AiTierResolution"
|
|
55
55
|
import type {AiTier, AiTierProvider, SavedLlmKeys} from "./AiTierResolution"
|
|
56
56
|
import {HttpError} from "@glassly/cloud-client"
|
|
57
|
+
import type {QuotaStatusPayload} from "@glassly/cloud-protocol"
|
|
57
58
|
import localDisplayManager from "./LocalDisplayManager"
|
|
58
59
|
import sttModelManager from "./STTModelManager"
|
|
59
60
|
import localTranslationService, {type LocalTranslationSubscription} from "./LocalTranslationService"
|
|
@@ -74,6 +75,7 @@ import {installedNotificationApps} from "./PhoneNotificationsSync"
|
|
|
74
75
|
import {
|
|
75
76
|
DERIVED_SETTINGS_KEYS,
|
|
76
77
|
SETTINGS_BRIDGE_KEYS,
|
|
78
|
+
input2dFrom,
|
|
77
79
|
isDerivedSettingsKey,
|
|
78
80
|
ringBatteryFrom,
|
|
79
81
|
selectSettingsBridgeKeys,
|
|
@@ -108,7 +110,12 @@ import {phonePhotoCoordinator} from "./PhonePhotoCoordinator"
|
|
|
108
110
|
import {phoneStreamCoordinator} from "./PhoneStreamCoordinator"
|
|
109
111
|
import {phoneVideoCoordinator} from "./PhoneVideoCoordinator"
|
|
110
112
|
import {runSentenceTtsPipeline} from "./SentenceTtsPipeline"
|
|
111
|
-
import {
|
|
113
|
+
import {
|
|
114
|
+
summarizeTranscriptionRoutes,
|
|
115
|
+
transcriptionDeliveryRoute,
|
|
116
|
+
type TranscriptionDeliveryRoute,
|
|
117
|
+
type TranscriptionSubscriberRoutes,
|
|
118
|
+
} from "./TranscriptionRouting"
|
|
112
119
|
import {prepareTtsSentences} from "./TtsTextSanitizer"
|
|
113
120
|
import {handleWifiAdbRequest} from "./WifiAdbRequest"
|
|
114
121
|
import {cloudClientService, setBlobNamespaceMigrator} from "./CloudClientService"
|
|
@@ -171,6 +178,8 @@ interface ConnectedMiniapp {
|
|
|
171
178
|
forceLocalTranscriptionStreams: Set<string>
|
|
172
179
|
/** Transcription/translation streams with at least one listener using the default/cloud route. */
|
|
173
180
|
cloudTranscriptionStreams: Set<string>
|
|
181
|
+
/** Transcription streams whose default-route listeners all refuse on-device fallback text. */
|
|
182
|
+
noFallbackTranscriptionStreams: Set<string>
|
|
174
183
|
sendMessage: (raw: string) => void
|
|
175
184
|
lastPongAt: number
|
|
176
185
|
unansweredPingRounds: number
|
|
@@ -213,6 +222,9 @@ interface SpeechRun {
|
|
|
213
222
|
|
|
214
223
|
type TranscriptionEventSource = "cloud" | "local"
|
|
215
224
|
|
|
225
|
+
/** The cloud snapshot miniapps see: the phone's connection state plus plan-quota blocks. */
|
|
226
|
+
type MiniappCloudStatus = CloudClientStatusSnapshot & {quotaBlocked: QuotaStatusPayload["feature"][]}
|
|
227
|
+
|
|
216
228
|
/** Streams that carry speech text and so have a cloud route and an on-device route. */
|
|
217
229
|
const isSpeechTextStream = (stream: string): boolean =>
|
|
218
230
|
stream.startsWith("transcription:") || stream.startsWith("translation:")
|
|
@@ -286,7 +298,7 @@ const MINIAPP_AUTH_REFRESH_MIN_DELAY_MS = 5_000
|
|
|
286
298
|
// (e.g. the cloud client had not finished its first-boot Core token exchange
|
|
287
299
|
// when the miniapp connected). Exponential from base, capped, so a transient
|
|
288
300
|
// not-yet-connected client self-heals within seconds instead of needing a full
|
|
289
|
-
// manual Cloud
|
|
301
|
+
// manual Cloud reconnect.
|
|
290
302
|
const MINIAPP_AUTH_RETRY_BASE_MS = 1_000
|
|
291
303
|
const MINIAPP_AUTH_RETRY_MAX_MS = 15_000
|
|
292
304
|
const FOREGROUND_LIVENESS_PROBE_TIMEOUT_MS = 2_500
|
|
@@ -1048,6 +1060,7 @@ class LocalMiniappRuntime {
|
|
|
1048
1060
|
subscriptions: new Set(),
|
|
1049
1061
|
forceLocalTranscriptionStreams: new Set(),
|
|
1050
1062
|
cloudTranscriptionStreams: new Set(),
|
|
1063
|
+
noFallbackTranscriptionStreams: new Set(),
|
|
1051
1064
|
sendMessage: sendFn,
|
|
1052
1065
|
lastPongAt: Date.now(),
|
|
1053
1066
|
unansweredPingRounds: 0,
|
|
@@ -1826,7 +1839,7 @@ class LocalMiniappRuntime {
|
|
|
1826
1839
|
// right after app start. CONNECT_ACK already went out without auth; keep
|
|
1827
1840
|
// trying to mint (and re-drive on cloud-connect) so the app authenticates
|
|
1828
1841
|
// to its backend on its own, instead of staying dead until a full manual
|
|
1829
|
-
// Cloud
|
|
1842
|
+
// Cloud reconnect re-runs the whole handshake.
|
|
1830
1843
|
this.deliverInitialMiniappAuth(packageName, authPromise, 0)
|
|
1831
1844
|
}
|
|
1832
1845
|
this.sendCloudStatusToMiniapp(packageName)
|
|
@@ -1988,7 +2001,7 @@ class LocalMiniappRuntime {
|
|
|
1988
2001
|
* When the cloud client (re)connects, immediately re-attempt the initial mint
|
|
1989
2002
|
* for any handshook app still missing its token, instead of waiting out the
|
|
1990
2003
|
* backoff. This is what turns "scan a dev app before the cloud is ready" from
|
|
1991
|
-
* "dead until you manually reconnect Cloud
|
|
2004
|
+
* "dead until you manually reconnect Cloud" into "authenticates on its own
|
|
1992
2005
|
* the instant the connection comes up".
|
|
1993
2006
|
*/
|
|
1994
2007
|
private redriveMiniappAuthDelivery(): void {
|
|
@@ -2101,7 +2114,10 @@ class LocalMiniappRuntime {
|
|
|
2101
2114
|
if (!app) return
|
|
2102
2115
|
|
|
2103
2116
|
const rawStreams = (payload.subscriptions ?? payload.streams) as
|
|
2104
|
-
| (
|
|
2117
|
+
| (
|
|
2118
|
+
| string
|
|
2119
|
+
| {stream: string; rate?: string; forceLocal?: boolean; includeCloud?: boolean; localFallback?: boolean}
|
|
2120
|
+
)[]
|
|
2105
2121
|
| undefined
|
|
2106
2122
|
// Normalize: objects like {stream: "location_stream", rate: "realtime"} → extract stream name
|
|
2107
2123
|
// "location_stream" is the rate-bearing alias for "location_update".
|
|
@@ -2110,6 +2126,7 @@ class LocalMiniappRuntime {
|
|
|
2110
2126
|
let requestedLocationRate: string | null = null
|
|
2111
2127
|
const forceLocalTranscriptionStreams = new Set<string>()
|
|
2112
2128
|
const cloudTranscriptionStreams = new Set<string>()
|
|
2129
|
+
const noFallbackTranscriptionStreams = new Set<string>()
|
|
2113
2130
|
const streams = rawStreams?.map((s) => {
|
|
2114
2131
|
if (typeof s === "object" && s !== null) {
|
|
2115
2132
|
if (s.stream === "location_stream") {
|
|
@@ -2123,6 +2140,7 @@ class LocalMiniappRuntime {
|
|
|
2123
2140
|
}
|
|
2124
2141
|
if (isSpeechTextStream(s.stream) && (s.forceLocal !== true || s.includeCloud === true)) {
|
|
2125
2142
|
cloudTranscriptionStreams.add(s.stream)
|
|
2143
|
+
if (s.localFallback === false) noFallbackTranscriptionStreams.add(s.stream)
|
|
2126
2144
|
}
|
|
2127
2145
|
return s.stream
|
|
2128
2146
|
}
|
|
@@ -2190,6 +2208,9 @@ class LocalMiniappRuntime {
|
|
|
2190
2208
|
app.cloudTranscriptionStreams = new Set(
|
|
2191
2209
|
[...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2192
2210
|
)
|
|
2211
|
+
app.noFallbackTranscriptionStreams = new Set(
|
|
2212
|
+
[...noFallbackTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2213
|
+
)
|
|
2193
2214
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
2194
2215
|
// The overlay app presents phone notifications on the glasses while it
|
|
2195
2216
|
// listens to them; recorded for every dashboard so the role can move.
|
|
@@ -2554,6 +2575,10 @@ class LocalMiniappRuntime {
|
|
|
2554
2575
|
return localDisplayManager.foregroundAppOnScreen()
|
|
2555
2576
|
case "ring_battery":
|
|
2556
2577
|
return ringBatteryFrom(useGlassesStore.getState())
|
|
2578
|
+
case "input_2d": {
|
|
2579
|
+
const controller = useSettingsStore.getState().getSetting(SETTINGS.default_controller.key)
|
|
2580
|
+
return input2dFrom(useGlassesStore.getState(), controller)
|
|
2581
|
+
}
|
|
2557
2582
|
}
|
|
2558
2583
|
}
|
|
2559
2584
|
|
|
@@ -2650,6 +2675,7 @@ class LocalMiniappRuntime {
|
|
|
2650
2675
|
(s) => `${s.ringConnected}:${s.ringBatteryLevel}:${s.controllerConnected}:${s.controllerBatteryLevel}`,
|
|
2651
2676
|
pushDerived,
|
|
2652
2677
|
)
|
|
2678
|
+
useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.default_controller.key), pushDerived)
|
|
2653
2679
|
}
|
|
2654
2680
|
|
|
2655
2681
|
/**
|
|
@@ -4863,6 +4889,7 @@ class LocalMiniappRuntime {
|
|
|
4863
4889
|
const previousSubscriptions = app.subscriptions
|
|
4864
4890
|
app.subscriptions = new Set(streams)
|
|
4865
4891
|
app.forceLocalTranscriptionStreams.clear()
|
|
4892
|
+
app.noFallbackTranscriptionStreams.clear()
|
|
4866
4893
|
app.cloudTranscriptionStreams = new Set(streams.filter(isSpeechTextStream))
|
|
4867
4894
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
4868
4895
|
|
|
@@ -4929,6 +4956,7 @@ class LocalMiniappRuntime {
|
|
|
4929
4956
|
let transcriptionLang: string | null = null
|
|
4930
4957
|
let forceLocalTranscriptionLang: string | null = null
|
|
4931
4958
|
let hasForceLocalTranscription = false
|
|
4959
|
+
let hasFallbackTranscription = false
|
|
4932
4960
|
const localTranslations: LocalTranslationSubscription[] = []
|
|
4933
4961
|
for (const [stream, subscribers] of this.activeStreamSubscribers()) {
|
|
4934
4962
|
if (subscribers.size === 0) continue
|
|
@@ -4960,6 +4988,7 @@ class LocalMiniappRuntime {
|
|
|
4960
4988
|
hasForceLocalTranscription = true
|
|
4961
4989
|
if (forceLocalTranscriptionLang === null) forceLocalTranscriptionLang = language
|
|
4962
4990
|
}
|
|
4991
|
+
if (routes.hasFallbackSubscriber) hasFallbackTranscription = true
|
|
4963
4992
|
if (routes.hasCloudSubscriber) cloudStreams.add(stream)
|
|
4964
4993
|
}
|
|
4965
4994
|
}
|
|
@@ -4974,6 +5003,7 @@ class LocalMiniappRuntime {
|
|
|
4974
5003
|
transcriptionLang !== null || localSourceLang !== null,
|
|
4975
5004
|
forceLocalTranscriptionLang ?? transcriptionLang ?? localSourceLang,
|
|
4976
5005
|
hasForceLocalTranscription || hasForceLocalTranslation,
|
|
5006
|
+
hasFallbackTranscription || localSourceLang !== null,
|
|
4977
5007
|
)
|
|
4978
5008
|
|
|
4979
5009
|
// Mirror the same set as typed AudioSubscription[]. When Soniox-direct is
|
|
@@ -5134,12 +5164,15 @@ class LocalMiniappRuntime {
|
|
|
5134
5164
|
if (q.feature !== "transcription") return
|
|
5135
5165
|
if (!q.blocked) {
|
|
5136
5166
|
this.lastSttQuotaNoticeAt = 0
|
|
5137
|
-
void localSttFallbackCoordinator.onSttQuotaBlockedChange(false)
|
|
5167
|
+
void localSttFallbackCoordinator.onSttQuotaBlockedChange(false).then(() => this.broadcastCloudStatus())
|
|
5138
5168
|
this.onQuotaRestored?.("transcription")
|
|
5139
5169
|
return
|
|
5140
5170
|
}
|
|
5141
5171
|
console.warn(`${LOG_TAG}: cloud transcription blocked by plan quota`)
|
|
5142
5172
|
void localSttFallbackCoordinator.onSttQuotaBlockedChange(true).then(() => {
|
|
5173
|
+
// Miniapps learn of the block through the status snapshot; the lens
|
|
5174
|
+
// notice below is for the wearer.
|
|
5175
|
+
this.broadcastCloudStatus()
|
|
5143
5176
|
const localSttActive = localSttFallbackCoordinator.isActive()
|
|
5144
5177
|
const now = Date.now()
|
|
5145
5178
|
if (now - this.lastSttQuotaNoticeAt >= STT_QUOTA_NOTICE_DEBOUNCE_MS) {
|
|
@@ -5383,7 +5416,7 @@ class LocalMiniappRuntime {
|
|
|
5383
5416
|
)
|
|
5384
5417
|
}
|
|
5385
5418
|
|
|
5386
|
-
private currentCloudStatus():
|
|
5419
|
+
private currentCloudStatus(): MiniappCloudStatus {
|
|
5387
5420
|
const base = cloudClientService.getStatus()
|
|
5388
5421
|
const fallbackActive =
|
|
5389
5422
|
(useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive) as boolean | undefined) ===
|
|
@@ -5394,6 +5427,7 @@ class LocalMiniappRuntime {
|
|
|
5394
5427
|
return {
|
|
5395
5428
|
status: base.status,
|
|
5396
5429
|
audioTransport: fallbackActive || sonioxDegraded ? "offline" : base.audioTransport,
|
|
5430
|
+
quotaBlocked: localSttFallbackCoordinator.isSttQuotaBlocked() ? ["transcription"] : [],
|
|
5397
5431
|
}
|
|
5398
5432
|
}
|
|
5399
5433
|
|
|
@@ -5543,7 +5577,7 @@ class LocalMiniappRuntime {
|
|
|
5543
5577
|
|
|
5544
5578
|
for (const packageName of matchedSubs) {
|
|
5545
5579
|
if (only !== undefined && packageName !== only) continue
|
|
5546
|
-
let transcriptionRoute:
|
|
5580
|
+
let transcriptionRoute: TranscriptionDeliveryRoute | undefined
|
|
5547
5581
|
if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
|
|
5548
5582
|
const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
|
|
5549
5583
|
transcriptionRoute =
|
|
@@ -5638,17 +5672,12 @@ class LocalMiniappRuntime {
|
|
|
5638
5672
|
})
|
|
5639
5673
|
}
|
|
5640
5674
|
|
|
5641
|
-
private appTranscriptionRoutesForSubscription(
|
|
5642
|
-
packageName: string,
|
|
5643
|
-
stream: string,
|
|
5644
|
-
): {
|
|
5645
|
-
cloud: boolean
|
|
5646
|
-
forceLocal: boolean
|
|
5647
|
-
} {
|
|
5675
|
+
private appTranscriptionRoutesForSubscription(packageName: string, stream: string): TranscriptionSubscriberRoutes {
|
|
5648
5676
|
const app = this.connectedApps.get(packageName)
|
|
5649
5677
|
return {
|
|
5650
5678
|
cloud: app?.cloudTranscriptionStreams.has(stream) === true,
|
|
5651
5679
|
forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
|
|
5680
|
+
localFallback: app?.noFallbackTranscriptionStreams.has(stream) !== true,
|
|
5652
5681
|
}
|
|
5653
5682
|
}
|
|
5654
5683
|
|
|
@@ -5666,21 +5695,20 @@ class LocalMiniappRuntime {
|
|
|
5666
5695
|
}
|
|
5667
5696
|
}
|
|
5668
5697
|
|
|
5669
|
-
private appTranscriptionRoutesForEvent(
|
|
5670
|
-
packageName: string,
|
|
5671
|
-
stream: string,
|
|
5672
|
-
): {
|
|
5673
|
-
cloud: boolean
|
|
5674
|
-
forceLocal: boolean
|
|
5675
|
-
} {
|
|
5698
|
+
private appTranscriptionRoutesForEvent(packageName: string, stream: string): TranscriptionSubscriberRoutes {
|
|
5676
5699
|
const app = this.connectedApps.get(packageName)
|
|
5677
5700
|
const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`
|
|
5701
|
+
const cloudExact = app?.cloudTranscriptionStreams.has(stream) === true
|
|
5702
|
+
const cloudAuto = app?.cloudTranscriptionStreams.has(autoStream) === true
|
|
5678
5703
|
return {
|
|
5679
|
-
cloud:
|
|
5680
|
-
app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
|
|
5704
|
+
cloud: cloudExact || cloudAuto,
|
|
5681
5705
|
forceLocal:
|
|
5682
5706
|
app?.forceLocalTranscriptionStreams.has(stream) === true ||
|
|
5683
5707
|
app?.forceLocalTranscriptionStreams.has(autoStream) === true,
|
|
5708
|
+
// Fallback stays accepted while any matching default subscription takes it.
|
|
5709
|
+
localFallback:
|
|
5710
|
+
(cloudExact && app?.noFallbackTranscriptionStreams.has(stream) !== true) ||
|
|
5711
|
+
(cloudAuto && app?.noFallbackTranscriptionStreams.has(autoStream) !== true),
|
|
5684
5712
|
}
|
|
5685
5713
|
}
|
|
5686
5714
|
|
|
@@ -8,7 +8,8 @@ import sttModelManager from "./STTModelManager"
|
|
|
8
8
|
* Decides whether on-device Sherpa STT should produce transcripts for local
|
|
9
9
|
* miniapps, instead of (or in addition to) cloud transcription. Local STT is
|
|
10
10
|
* active when the cloud is unavailable (disconnected or refusing STT for plan
|
|
11
|
-
* quota) or any subscription
|
|
11
|
+
* quota) and some subscriber accepts fallback text, or any subscription
|
|
12
|
+
* requires `forceLocal`.
|
|
12
13
|
*
|
|
13
14
|
* Activation flips `localSttFallbackActive` in the settings store, which
|
|
14
15
|
* the bluetooth-sdk native side reads to decide whether to feed PCM into
|
|
@@ -37,6 +38,8 @@ class LocalSttFallbackCoordinator {
|
|
|
37
38
|
|
|
38
39
|
private hasTranscriptionSubscription = false
|
|
39
40
|
private hasForceLocalSubscription = false
|
|
41
|
+
/** Some default-route subscriber takes on-device text while the primary is down. */
|
|
42
|
+
private hasFallbackSubscription = false
|
|
40
43
|
private activeLanguage: string | null = null
|
|
41
44
|
/**
|
|
42
45
|
* Default to "cloud is up" so we never accidentally activate local STT before
|
|
@@ -123,10 +126,18 @@ class LocalSttFallbackCoordinator {
|
|
|
123
126
|
return this.localActive ? this.currentReason() : null
|
|
124
127
|
}
|
|
125
128
|
|
|
126
|
-
onSubscriptionChange(
|
|
127
|
-
|
|
129
|
+
onSubscriptionChange(
|
|
130
|
+
hasTranscription: boolean,
|
|
131
|
+
language: string | null,
|
|
132
|
+
hasForceLocal = false,
|
|
133
|
+
acceptsFallback = true,
|
|
134
|
+
): void {
|
|
135
|
+
this.log(
|
|
136
|
+
`onSubscriptionChange(hasTx=${hasTranscription}, lang=${language}, forceLocal=${hasForceLocal}, fallback=${acceptsFallback})`,
|
|
137
|
+
)
|
|
128
138
|
this.hasTranscriptionSubscription = hasTranscription
|
|
129
139
|
this.hasForceLocalSubscription = hasTranscription && hasForceLocal
|
|
140
|
+
this.hasFallbackSubscription = hasTranscription && acceptsFallback
|
|
130
141
|
this.activeLanguage = hasTranscription ? language : null
|
|
131
142
|
void this.reconcile()
|
|
132
143
|
}
|
|
@@ -256,7 +267,10 @@ class LocalSttFallbackCoordinator {
|
|
|
256
267
|
}
|
|
257
268
|
|
|
258
269
|
private shouldUseLocalStt(): boolean {
|
|
259
|
-
return
|
|
270
|
+
return (
|
|
271
|
+
this.hasTranscriptionSubscription &&
|
|
272
|
+
(this.hasForceLocalSubscription || (this.hasFallbackSubscription && !this.primaryUp()))
|
|
273
|
+
)
|
|
260
274
|
}
|
|
261
275
|
|
|
262
276
|
private log(msg: string): void {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* miniapp → SDK → LocalMiniappRuntime → photo runtime hook
|
|
6
6
|
* → coordinator.takePhoto(packageName, opts)
|
|
7
7
|
* ├── (precheck) glasses connected + hasCamera
|
|
8
|
-
* ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud
|
|
8
|
+
* ├── cloudClientService.startManagedPhoto → {requestId, uploadUrl, readUrl} (cloud runtime presign)
|
|
9
9
|
* ├── BluetoothSdk.requestPhoto(requestId, size, uploadUrl, compress, sound)
|
|
10
10
|
* └── race:
|
|
11
11
|
* - cloudClientService.awaitManagedPhotoReady(requestId) resolves on photo.ready push
|
|
@@ -179,8 +179,8 @@ export class PhonePhotoCoordinator {
|
|
|
179
179
|
// high-capacity tier and let the glasses ignore the public size when mode=text.
|
|
180
180
|
const captureSize = opts.size ?? "medium"
|
|
181
181
|
|
|
182
|
-
// 1) Presign via the cloud
|
|
183
|
-
// ONLY the cloud
|
|
182
|
+
// 1) Presign via the cloud managed-photo service. Local miniapps use
|
|
183
|
+
// ONLY the cloud path: the runtime presigns upload+read URLs and the
|
|
184
184
|
// phone (as the device controller) delivers the bytes; the legacy
|
|
185
185
|
// backend_url mint is gone from this flow.
|
|
186
186
|
let requestId: string
|
|
@@ -419,7 +419,7 @@ export class PhonePhotoCoordinator {
|
|
|
419
419
|
// Abort the in-flight long-poll first so we don't double-resolve.
|
|
420
420
|
entry.abort.abort()
|
|
421
421
|
entry.reject(new PhotoError(errorCode || "GLASSES_ERROR", errorMessage || "Glasses error", "capture", "ble"))
|
|
422
|
-
// cloud
|
|
422
|
+
// cloud pending photo requests TTL out on their own; nothing to free.
|
|
423
423
|
}
|
|
424
424
|
|
|
425
425
|
private toPhotoError(err: unknown, fallbackCode: string, stage?: PhotoStage, transport?: PhotoTransport): PhotoError {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Architecture:
|
|
5
5
|
* miniapp → SDK → LocalMiniappRuntime → coordinator
|
|
6
6
|
* coordinator → BluetoothSdk (BLE → glasses publisher)
|
|
7
|
-
* coordinator ↔ cloudStreamApi (managed only, cloud
|
|
7
|
+
* coordinator ↔ cloudStreamApi (managed only, cloud runtime provisioning)
|
|
8
8
|
* coordinator ↔ StreamLifecycleController (keep-alive heartbeat)
|
|
9
9
|
* coordinator → status listeners → routed back to miniapp(s)
|
|
10
10
|
*
|
|
@@ -26,7 +26,7 @@ let lastFailure: {code: string; stage: string} | null = null
|
|
|
26
26
|
let lastConnectionState: string | null = null
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
* Keep Cloud
|
|
29
|
+
* Keep Cloud's canonical support snapshot current without creating an event
|
|
30
30
|
* stream from noisy store updates. Only the explicitly selected fields below
|
|
31
31
|
* leave the phone; Wi-Fi, Bluetooth addresses, location and diagnostics do not.
|
|
32
32
|
*/
|
|
@@ -110,7 +110,7 @@ async function sendCurrentSnapshot(force: boolean, generation = syncGeneration):
|
|
|
110
110
|
consecutiveSendFailures += 1
|
|
111
111
|
retryDelayMs = retryDelayForSendFailure(consecutiveSendFailures)
|
|
112
112
|
console.warn(
|
|
113
|
-
`supportProfile: Cloud
|
|
113
|
+
`supportProfile: Cloud update failed (retry in ${Math.round(retryDelayMs / 1_000)}s):`,
|
|
114
114
|
error instanceof Error ? error.message : error,
|
|
115
115
|
)
|
|
116
116
|
} finally {
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
export interface TranscriptionRouteSummary {
|
|
2
2
|
hasCloudSubscriber: boolean
|
|
3
3
|
hasForceLocalSubscriber: boolean
|
|
4
|
+
/** A default-route subscriber that takes on-device text while the primary is down. */
|
|
5
|
+
hasFallbackSubscriber: boolean
|
|
4
6
|
}
|
|
5
7
|
|
|
6
8
|
export interface TranscriptionSubscriberRoutes {
|
|
7
9
|
cloud: boolean
|
|
8
10
|
forceLocal: boolean
|
|
11
|
+
/** Default-route fallback acceptance; absent means accepted. */
|
|
12
|
+
localFallback?: boolean
|
|
9
13
|
}
|
|
10
14
|
|
|
11
|
-
|
|
15
|
+
/** "fallback" is on-device text serving the default route because the primary is down. */
|
|
16
|
+
export type TranscriptionDeliveryRoute = "default" | "forceLocal" | "fallback" | "all"
|
|
12
17
|
|
|
13
18
|
export function summarizeTranscriptionRoutes<T>(
|
|
14
19
|
subscribers: Iterable<T>,
|
|
@@ -16,12 +21,14 @@ export function summarizeTranscriptionRoutes<T>(
|
|
|
16
21
|
): TranscriptionRouteSummary {
|
|
17
22
|
let hasCloudSubscriber = false
|
|
18
23
|
let hasForceLocalSubscriber = false
|
|
24
|
+
let hasFallbackSubscriber = false
|
|
19
25
|
for (const subscriber of subscribers) {
|
|
20
26
|
const routes = routesFor(subscriber)
|
|
21
27
|
if (routes.cloud) hasCloudSubscriber = true
|
|
22
28
|
if (routes.forceLocal) hasForceLocalSubscriber = true
|
|
29
|
+
if (routes.cloud && routes.localFallback !== false) hasFallbackSubscriber = true
|
|
23
30
|
}
|
|
24
|
-
return {hasCloudSubscriber, hasForceLocalSubscriber}
|
|
31
|
+
return {hasCloudSubscriber, hasForceLocalSubscriber, hasFallbackSubscriber}
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
/**
|
|
@@ -41,7 +48,8 @@ export function transcriptionDeliveryRoute(
|
|
|
41
48
|
): TranscriptionDeliveryRoute | null {
|
|
42
49
|
if (source === "cloud") return routes.cloud ? "default" : null
|
|
43
50
|
if (primaryUp) return routes.forceLocal ? "forceLocal" : null
|
|
44
|
-
|
|
51
|
+
const fallback = routes.cloud && routes.localFallback !== false
|
|
52
|
+
if (fallback && routes.forceLocal) return "all"
|
|
45
53
|
if (routes.forceLocal) return "forceLocal"
|
|
46
|
-
return
|
|
54
|
+
return fallback ? "fallback" : null
|
|
47
55
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Managed-stream provisioning over the cloud
|
|
2
|
+
* Managed-stream provisioning over the cloud runtime (cloud-client REST):
|
|
3
3
|
*
|
|
4
4
|
* POST /api/camera/stream -> provision (Cloudflare live input)
|
|
5
5
|
* GET /api/camera/stream/:id -> provider's ingest status
|
|
@@ -61,7 +61,7 @@ export class StreamApiError extends Error {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** The cloud
|
|
64
|
+
/** The cloud runtime's ManagedStream wire shape (open records, see protocol). */
|
|
65
65
|
interface ManagedStreamWire {
|
|
66
66
|
streamId: string
|
|
67
67
|
ingest: {
|
|
@@ -79,7 +79,7 @@ interface ManagedStreamWire {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* Provision a managed stream on the cloud
|
|
82
|
+
* Provision a managed stream on the cloud runtime. Returns ready-to-publish
|
|
83
83
|
* ingest URLs (credentials embedded) plus the playback URLs.
|
|
84
84
|
*/
|
|
85
85
|
export async function provisionManagedStream(
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const GESTURE_DOT_GLYPH_SIZE = 24
|
|
6
6
|
|
|
7
|
-
export type GestureDotGlyph = "dot" | "up" | "down" | "hold" | "blank"
|
|
7
|
+
export type GestureDotGlyph = "dot" | "double" | "ring" | "doubleRing" | "up" | "down" | "left" | "right" | "hold" | "blank"
|
|
8
8
|
|
|
9
9
|
export const GESTURE_DOT_GLYPHS: Record<GestureDotGlyph, string> = {
|
|
10
10
|
dot: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALklEQVR4nM2QsQ0AAAjC/P/pmrhqZ+xIF6DqBQyWbwO3gduAmKjQuj7QL/ETEzQmMptl7Q9NBAAAAABJRU5ErkJggg==",
|
|
11
|
+
double: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAG0lEQVR4nGNgGD7gPxjgYkN4yOyBlcDp3GEBAB3sd4lPaQxDAAAAAElFTkSuQmCC",
|
|
12
|
+
ring: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAANElEQVR4nGNgGBTgPxjgEseUgQphyCD4qDL/kSX+Y9WApoWqEjgsx+lc3B7EHSS4A3EgAAC+cEu1JTNREQAAAABJRU5ErkJggg==",
|
|
13
|
+
doubleRing: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIklEQVR4nGNgGD7gPxjgYkN4yGwGmBA6SV0JHJbjdO6wAADBMlep8zl7TAAAAABJRU5ErkJggg==",
|
|
11
14
|
up: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAMElEQVR4nGNgoCv4////f1zi2GT+/4ciTHE4RhdHIlDFUUhkcTQKQx9WBw8PCVoBAIcbOcf3xeeBAAAAAElFTkSuQmCC",
|
|
12
15
|
down: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAALUlEQVR4nGNgoC/4/3+4SkBFUCkkNjKJIoMg0GRgGEMGgrC4CQSwuxanR2gEAO1/Ocfgp8uvAAAAAElFTkSuQmCC",
|
|
16
|
+
left: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAI0lEQVR4nGNgoCv4j0sch8RAiTP8RwPESOAwajDI4JKgGQAAKO05x7DqJxIAAAAASUVORK5CYII=",
|
|
17
|
+
right: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAIElEQVR4nGNgoDf4j1NiMMn8RwOEJYizZKDEcQc7bQAAS7w5x+ro2lsAAAAASUVORK5CYII=",
|
|
13
18
|
hold: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAR0lEQVR4nGNgIAj+gwF2UUy5//+xyyDzcLGRedjMhTEwXAKl0Z0Lof7DKbgMhBpQCZzOxedBHEGCMxAJBTXhKGRAA9hFMQEANr7jHQU9fO0AAAAASUVORK5CYII=",
|
|
14
19
|
blank: "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAAAAADFHGIkAAAAD0lEQVR4nGNgGAWjgPoAAAJYAAGgxYwDAAAAAElFTkSuQmCC",
|
|
15
20
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Meta account sign-in a Neural Band claim needs, ported from callbacked/kinesis
|
|
3
|
+
* (MIT, Copyright (c) 2026 callbacked): Sources/Kinesis/MetaAuth.swift.
|
|
4
|
+
*
|
|
5
|
+
* The chain: an anonymous tokens query, a WebView sign-in on auth.meta.com whose
|
|
6
|
+
* fb-viewapp://…frl_login redirect carries a token and a blob, a blob decrypt, and
|
|
7
|
+
* the ar-genai exchange that yields the user session. The session then goes to the
|
|
8
|
+
* native Bluetooth SDK (keychain / keystore), which runs the ownership exchanges;
|
|
9
|
+
* nothing here persists it.
|
|
10
|
+
*/
|
|
11
|
+
import {sha256Hex} from "./sha256"
|
|
12
|
+
|
|
13
|
+
// Meta's own client identifiers, the same in every copy of Meta's apps. They name the
|
|
14
|
+
// app that asks, not a person. They are not secrets, and no account or session is in them.
|
|
15
|
+
const FRL_CLIENT = "FRL|388177446008673|083800dd7efbbd42eab18c9886d79c18"
|
|
16
|
+
const AR_CLIENT = "AR|306760944872162|a919421a55a8ea18080ab2f10f57f1be"
|
|
17
|
+
const TOKENS_QUERY_URL = "https://meta.graph.meta.com/webview_tokens_query"
|
|
18
|
+
const BLOBS_DECRYPT_URL = "https://meta.graph.meta.com/webview_blobs_decrypt"
|
|
19
|
+
const LOGIN_URL = "https://ar-genai.graph.meta.com/login"
|
|
20
|
+
const AUTH_ENTRY_BASE =
|
|
21
|
+
"https://auth.meta.com/?native_app_id=388177446008673&source_app_id=388177446008673&native_sso_etoken="
|
|
22
|
+
|
|
23
|
+
export class MetaAuthError extends Error {}
|
|
24
|
+
|
|
25
|
+
export interface MetaSsoTokens {
|
|
26
|
+
nativeSsoToken: string
|
|
27
|
+
etoken: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface MetaSession {
|
|
31
|
+
accessToken: string
|
|
32
|
+
userId: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function authEntryUrl(tokens: MetaSsoTokens): string {
|
|
36
|
+
return AUTH_ENTRY_BASE + encodeURIComponent(tokens.etoken)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The redirect that ends the sign-in: a custom scheme, or the frl_login marker on https. */
|
|
40
|
+
export function isLoginCallback(url: string): boolean {
|
|
41
|
+
return url.toLowerCase().startsWith("fb-viewapp:") || url.includes("frl_login")
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function callbackQuery(url: string): {token?: string; blob?: string} {
|
|
45
|
+
const query = url.includes("?") ? url.slice(url.indexOf("?") + 1) : ""
|
|
46
|
+
const out: {token?: string; blob?: string} = {}
|
|
47
|
+
for (const pair of query.split("&")) {
|
|
48
|
+
const [key, value = ""] = pair.split("=")
|
|
49
|
+
if (key === "token") out.token = decodeURIComponent(value)
|
|
50
|
+
if (key === "blob") out.blob = decodeURIComponent(value)
|
|
51
|
+
}
|
|
52
|
+
return out
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** The first 16 hex digits of the request token's SHA-256, which the callback must present. */
|
|
56
|
+
export function expectedCallbackToken(nativeSsoToken: string): string {
|
|
57
|
+
return sha256Hex(nativeSsoToken).slice(0, 16)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function callbackMatches(token: string | undefined, nativeSsoToken: string): boolean {
|
|
61
|
+
if (!token || token.length !== 16) return false
|
|
62
|
+
const expected = expectedCallbackToken(nativeSsoToken)
|
|
63
|
+
let difference = 0
|
|
64
|
+
for (let i = 0; i < 16; i++) difference |= token.charCodeAt(i) ^ expected.charCodeAt(i)
|
|
65
|
+
return difference === 0
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function makeLsd(): string {
|
|
69
|
+
let digits = ""
|
|
70
|
+
for (let i = 0; i < 6; i++) digits += Math.floor(Math.random() * 10)
|
|
71
|
+
return "S0." + digits
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function jazoest(lsd: string): string {
|
|
75
|
+
let sum = 0
|
|
76
|
+
for (const char of lsd) sum += char.codePointAt(0) ?? 0
|
|
77
|
+
return "2" + sum
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function multipartBody(fields: [string, string][], boundary: string): string {
|
|
81
|
+
let body = ""
|
|
82
|
+
for (const [name, value] of fields) {
|
|
83
|
+
body += `--${boundary}\r\nContent-Disposition: form-data; name="${name}"\r\n\r\n${value}\r\n`
|
|
84
|
+
}
|
|
85
|
+
return body + `--${boundary}--\r\n`
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function urlForm(fields: [string, string][]): string {
|
|
89
|
+
return fields.map(([name, value]) => `${name}=${encodeURIComponent(value)}`).join("&")
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type Json = Record<string, unknown>
|
|
93
|
+
|
|
94
|
+
async function post(url: string, headers: Record<string, string>, body: string): Promise<Json> {
|
|
95
|
+
const response = await fetch(url, {method: "POST", headers, body})
|
|
96
|
+
let json: Json | null = null
|
|
97
|
+
try {
|
|
98
|
+
json = (await response.json()) as Json
|
|
99
|
+
} catch {
|
|
100
|
+
json = null
|
|
101
|
+
}
|
|
102
|
+
if (response.status !== 200 || json === null) {
|
|
103
|
+
throw new MetaAuthError(`Meta sign-in failed (HTTP ${response.status}). Try again.`)
|
|
104
|
+
}
|
|
105
|
+
if (json.error !== undefined) {
|
|
106
|
+
const error = json.error as {code?: number; error_subcode?: number} | null
|
|
107
|
+
throw new MetaAuthError(
|
|
108
|
+
`Meta sign-in failed (code ${error?.code ?? "-"}, subcode ${error?.error_subcode ?? "-"}). Try again.`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return json
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function postMultipart(url: string, fields: [string, string][]): Promise<Json> {
|
|
115
|
+
const boundary = `glassly.form.${Date.now().toString(36)}${Math.random().toString(36).slice(2)}`
|
|
116
|
+
return post(
|
|
117
|
+
url,
|
|
118
|
+
{
|
|
119
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
120
|
+
"Accept": "application/json",
|
|
121
|
+
"Origin": "https://auth.meta.com",
|
|
122
|
+
},
|
|
123
|
+
multipartBody(fields, boundary),
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Step 1: the anonymous WebView tokens query. */
|
|
128
|
+
export async function tokensQuery(): Promise<MetaSsoTokens> {
|
|
129
|
+
const lsd = makeLsd()
|
|
130
|
+
const json = await postMultipart(TOKENS_QUERY_URL, [
|
|
131
|
+
["access_token", FRL_CLIENT],
|
|
132
|
+
["lsd", lsd],
|
|
133
|
+
["jazoest", jazoest(lsd)],
|
|
134
|
+
])
|
|
135
|
+
const nativeSsoToken = json.native_sso_token
|
|
136
|
+
const etoken = json.native_sso_etoken
|
|
137
|
+
if (typeof nativeSsoToken !== "string" || !nativeSsoToken || typeof etoken !== "string" || !etoken) {
|
|
138
|
+
throw new MetaAuthError("Meta didn't return sign-in tokens. Try again.")
|
|
139
|
+
}
|
|
140
|
+
return {nativeSsoToken, etoken}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Step 3: decrypt the login blob into the FRL access token. */
|
|
144
|
+
export async function decryptBlob(blob: string, requestToken: string): Promise<string> {
|
|
145
|
+
const lsd = makeLsd()
|
|
146
|
+
const json = await postMultipart(BLOBS_DECRYPT_URL, [
|
|
147
|
+
["blob", blob],
|
|
148
|
+
["request_token", requestToken],
|
|
149
|
+
["access_token", FRL_CLIENT],
|
|
150
|
+
["lsd", lsd],
|
|
151
|
+
["jazoest", jazoest(lsd)],
|
|
152
|
+
])
|
|
153
|
+
const token = json.access_token
|
|
154
|
+
if (typeof token !== "string" || !token) throw new MetaAuthError("Meta didn't confirm the sign-in. Try again.")
|
|
155
|
+
return token
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function randomId(): string {
|
|
159
|
+
const hex = () =>
|
|
160
|
+
Math.floor(Math.random() * 0x10000)
|
|
161
|
+
.toString(16)
|
|
162
|
+
.padStart(4, "0")
|
|
163
|
+
return `${hex()}${hex()}-${hex()}-${hex()}-${hex()}-${hex()}${hex()}${hex()}`.toUpperCase()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Step 4: exchange the FRL token for the ar user session. */
|
|
167
|
+
export async function login(frlAccessToken: string): Promise<MetaSession> {
|
|
168
|
+
const json = await post(
|
|
169
|
+
LOGIN_URL,
|
|
170
|
+
{
|
|
171
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
172
|
+
"Accept": "application/json",
|
|
173
|
+
"Authorization": "OAuth " + AR_CLIENT,
|
|
174
|
+
},
|
|
175
|
+
urlForm([
|
|
176
|
+
["frl_access_token", frlAccessToken],
|
|
177
|
+
["logging_session_id", randomId()],
|
|
178
|
+
["format", "json"],
|
|
179
|
+
["device_id", randomId()],
|
|
180
|
+
["generate_session_cookies", "1"],
|
|
181
|
+
["generate_analytics_claim", "1"],
|
|
182
|
+
["method", "POST"],
|
|
183
|
+
]),
|
|
184
|
+
)
|
|
185
|
+
const accessToken = json.access_token
|
|
186
|
+
const rawUserId = json.user_id
|
|
187
|
+
const userId = typeof rawUserId === "string" ? rawUserId : typeof rawUserId === "number" ? String(rawUserId) : ""
|
|
188
|
+
if (typeof accessToken !== "string" || !accessToken || !userId) {
|
|
189
|
+
throw new MetaAuthError("Meta didn't return an account session. Try again.")
|
|
190
|
+
}
|
|
191
|
+
return {accessToken, userId}
|
|
192
|
+
}
|