@glassly/engine 0.3.2-beta.572 → 0.3.2-beta.597
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 +26 -7
- 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/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 +46 -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/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"
|
|
@@ -108,7 +109,12 @@ import {phonePhotoCoordinator} from "./PhonePhotoCoordinator"
|
|
|
108
109
|
import {phoneStreamCoordinator} from "./PhoneStreamCoordinator"
|
|
109
110
|
import {phoneVideoCoordinator} from "./PhoneVideoCoordinator"
|
|
110
111
|
import {runSentenceTtsPipeline} from "./SentenceTtsPipeline"
|
|
111
|
-
import {
|
|
112
|
+
import {
|
|
113
|
+
summarizeTranscriptionRoutes,
|
|
114
|
+
transcriptionDeliveryRoute,
|
|
115
|
+
type TranscriptionDeliveryRoute,
|
|
116
|
+
type TranscriptionSubscriberRoutes,
|
|
117
|
+
} from "./TranscriptionRouting"
|
|
112
118
|
import {prepareTtsSentences} from "./TtsTextSanitizer"
|
|
113
119
|
import {handleWifiAdbRequest} from "./WifiAdbRequest"
|
|
114
120
|
import {cloudClientService, setBlobNamespaceMigrator} from "./CloudClientService"
|
|
@@ -171,6 +177,8 @@ interface ConnectedMiniapp {
|
|
|
171
177
|
forceLocalTranscriptionStreams: Set<string>
|
|
172
178
|
/** Transcription/translation streams with at least one listener using the default/cloud route. */
|
|
173
179
|
cloudTranscriptionStreams: Set<string>
|
|
180
|
+
/** Transcription streams whose default-route listeners all refuse on-device fallback text. */
|
|
181
|
+
noFallbackTranscriptionStreams: Set<string>
|
|
174
182
|
sendMessage: (raw: string) => void
|
|
175
183
|
lastPongAt: number
|
|
176
184
|
unansweredPingRounds: number
|
|
@@ -213,6 +221,9 @@ interface SpeechRun {
|
|
|
213
221
|
|
|
214
222
|
type TranscriptionEventSource = "cloud" | "local"
|
|
215
223
|
|
|
224
|
+
/** The cloud snapshot miniapps see: the phone's connection state plus plan-quota blocks. */
|
|
225
|
+
type MiniappCloudStatus = CloudClientStatusSnapshot & {quotaBlocked: QuotaStatusPayload["feature"][]}
|
|
226
|
+
|
|
216
227
|
/** Streams that carry speech text and so have a cloud route and an on-device route. */
|
|
217
228
|
const isSpeechTextStream = (stream: string): boolean =>
|
|
218
229
|
stream.startsWith("transcription:") || stream.startsWith("translation:")
|
|
@@ -286,7 +297,7 @@ const MINIAPP_AUTH_REFRESH_MIN_DELAY_MS = 5_000
|
|
|
286
297
|
// (e.g. the cloud client had not finished its first-boot Core token exchange
|
|
287
298
|
// when the miniapp connected). Exponential from base, capped, so a transient
|
|
288
299
|
// not-yet-connected client self-heals within seconds instead of needing a full
|
|
289
|
-
// manual Cloud
|
|
300
|
+
// manual Cloud reconnect.
|
|
290
301
|
const MINIAPP_AUTH_RETRY_BASE_MS = 1_000
|
|
291
302
|
const MINIAPP_AUTH_RETRY_MAX_MS = 15_000
|
|
292
303
|
const FOREGROUND_LIVENESS_PROBE_TIMEOUT_MS = 2_500
|
|
@@ -1048,6 +1059,7 @@ class LocalMiniappRuntime {
|
|
|
1048
1059
|
subscriptions: new Set(),
|
|
1049
1060
|
forceLocalTranscriptionStreams: new Set(),
|
|
1050
1061
|
cloudTranscriptionStreams: new Set(),
|
|
1062
|
+
noFallbackTranscriptionStreams: new Set(),
|
|
1051
1063
|
sendMessage: sendFn,
|
|
1052
1064
|
lastPongAt: Date.now(),
|
|
1053
1065
|
unansweredPingRounds: 0,
|
|
@@ -1826,7 +1838,7 @@ class LocalMiniappRuntime {
|
|
|
1826
1838
|
// right after app start. CONNECT_ACK already went out without auth; keep
|
|
1827
1839
|
// trying to mint (and re-drive on cloud-connect) so the app authenticates
|
|
1828
1840
|
// to its backend on its own, instead of staying dead until a full manual
|
|
1829
|
-
// Cloud
|
|
1841
|
+
// Cloud reconnect re-runs the whole handshake.
|
|
1830
1842
|
this.deliverInitialMiniappAuth(packageName, authPromise, 0)
|
|
1831
1843
|
}
|
|
1832
1844
|
this.sendCloudStatusToMiniapp(packageName)
|
|
@@ -1988,7 +2000,7 @@ class LocalMiniappRuntime {
|
|
|
1988
2000
|
* When the cloud client (re)connects, immediately re-attempt the initial mint
|
|
1989
2001
|
* for any handshook app still missing its token, instead of waiting out the
|
|
1990
2002
|
* backoff. This is what turns "scan a dev app before the cloud is ready" from
|
|
1991
|
-
* "dead until you manually reconnect Cloud
|
|
2003
|
+
* "dead until you manually reconnect Cloud" into "authenticates on its own
|
|
1992
2004
|
* the instant the connection comes up".
|
|
1993
2005
|
*/
|
|
1994
2006
|
private redriveMiniappAuthDelivery(): void {
|
|
@@ -2101,7 +2113,10 @@ class LocalMiniappRuntime {
|
|
|
2101
2113
|
if (!app) return
|
|
2102
2114
|
|
|
2103
2115
|
const rawStreams = (payload.subscriptions ?? payload.streams) as
|
|
2104
|
-
| (
|
|
2116
|
+
| (
|
|
2117
|
+
| string
|
|
2118
|
+
| {stream: string; rate?: string; forceLocal?: boolean; includeCloud?: boolean; localFallback?: boolean}
|
|
2119
|
+
)[]
|
|
2105
2120
|
| undefined
|
|
2106
2121
|
// Normalize: objects like {stream: "location_stream", rate: "realtime"} → extract stream name
|
|
2107
2122
|
// "location_stream" is the rate-bearing alias for "location_update".
|
|
@@ -2110,6 +2125,7 @@ class LocalMiniappRuntime {
|
|
|
2110
2125
|
let requestedLocationRate: string | null = null
|
|
2111
2126
|
const forceLocalTranscriptionStreams = new Set<string>()
|
|
2112
2127
|
const cloudTranscriptionStreams = new Set<string>()
|
|
2128
|
+
const noFallbackTranscriptionStreams = new Set<string>()
|
|
2113
2129
|
const streams = rawStreams?.map((s) => {
|
|
2114
2130
|
if (typeof s === "object" && s !== null) {
|
|
2115
2131
|
if (s.stream === "location_stream") {
|
|
@@ -2123,6 +2139,7 @@ class LocalMiniappRuntime {
|
|
|
2123
2139
|
}
|
|
2124
2140
|
if (isSpeechTextStream(s.stream) && (s.forceLocal !== true || s.includeCloud === true)) {
|
|
2125
2141
|
cloudTranscriptionStreams.add(s.stream)
|
|
2142
|
+
if (s.localFallback === false) noFallbackTranscriptionStreams.add(s.stream)
|
|
2126
2143
|
}
|
|
2127
2144
|
return s.stream
|
|
2128
2145
|
}
|
|
@@ -2190,6 +2207,9 @@ class LocalMiniappRuntime {
|
|
|
2190
2207
|
app.cloudTranscriptionStreams = new Set(
|
|
2191
2208
|
[...cloudTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2192
2209
|
)
|
|
2210
|
+
app.noFallbackTranscriptionStreams = new Set(
|
|
2211
|
+
[...noFallbackTranscriptionStreams].filter((stream) => app.subscriptions.has(stream)),
|
|
2212
|
+
)
|
|
2193
2213
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
2194
2214
|
// The overlay app presents phone notifications on the glasses while it
|
|
2195
2215
|
// listens to them; recorded for every dashboard so the role can move.
|
|
@@ -4863,6 +4883,7 @@ class LocalMiniappRuntime {
|
|
|
4863
4883
|
const previousSubscriptions = app.subscriptions
|
|
4864
4884
|
app.subscriptions = new Set(streams)
|
|
4865
4885
|
app.forceLocalTranscriptionStreams.clear()
|
|
4886
|
+
app.noFallbackTranscriptionStreams.clear()
|
|
4866
4887
|
app.cloudTranscriptionStreams = new Set(streams.filter(isSpeechTextStream))
|
|
4867
4888
|
this.replaceStreamSubscribers(packageName, previousSubscriptions, app.subscriptions)
|
|
4868
4889
|
|
|
@@ -4929,6 +4950,7 @@ class LocalMiniappRuntime {
|
|
|
4929
4950
|
let transcriptionLang: string | null = null
|
|
4930
4951
|
let forceLocalTranscriptionLang: string | null = null
|
|
4931
4952
|
let hasForceLocalTranscription = false
|
|
4953
|
+
let hasFallbackTranscription = false
|
|
4932
4954
|
const localTranslations: LocalTranslationSubscription[] = []
|
|
4933
4955
|
for (const [stream, subscribers] of this.activeStreamSubscribers()) {
|
|
4934
4956
|
if (subscribers.size === 0) continue
|
|
@@ -4960,6 +4982,7 @@ class LocalMiniappRuntime {
|
|
|
4960
4982
|
hasForceLocalTranscription = true
|
|
4961
4983
|
if (forceLocalTranscriptionLang === null) forceLocalTranscriptionLang = language
|
|
4962
4984
|
}
|
|
4985
|
+
if (routes.hasFallbackSubscriber) hasFallbackTranscription = true
|
|
4963
4986
|
if (routes.hasCloudSubscriber) cloudStreams.add(stream)
|
|
4964
4987
|
}
|
|
4965
4988
|
}
|
|
@@ -4974,6 +4997,7 @@ class LocalMiniappRuntime {
|
|
|
4974
4997
|
transcriptionLang !== null || localSourceLang !== null,
|
|
4975
4998
|
forceLocalTranscriptionLang ?? transcriptionLang ?? localSourceLang,
|
|
4976
4999
|
hasForceLocalTranscription || hasForceLocalTranslation,
|
|
5000
|
+
hasFallbackTranscription || localSourceLang !== null,
|
|
4977
5001
|
)
|
|
4978
5002
|
|
|
4979
5003
|
// Mirror the same set as typed AudioSubscription[]. When Soniox-direct is
|
|
@@ -5134,12 +5158,15 @@ class LocalMiniappRuntime {
|
|
|
5134
5158
|
if (q.feature !== "transcription") return
|
|
5135
5159
|
if (!q.blocked) {
|
|
5136
5160
|
this.lastSttQuotaNoticeAt = 0
|
|
5137
|
-
void localSttFallbackCoordinator.onSttQuotaBlockedChange(false)
|
|
5161
|
+
void localSttFallbackCoordinator.onSttQuotaBlockedChange(false).then(() => this.broadcastCloudStatus())
|
|
5138
5162
|
this.onQuotaRestored?.("transcription")
|
|
5139
5163
|
return
|
|
5140
5164
|
}
|
|
5141
5165
|
console.warn(`${LOG_TAG}: cloud transcription blocked by plan quota`)
|
|
5142
5166
|
void localSttFallbackCoordinator.onSttQuotaBlockedChange(true).then(() => {
|
|
5167
|
+
// Miniapps learn of the block through the status snapshot; the lens
|
|
5168
|
+
// notice below is for the wearer.
|
|
5169
|
+
this.broadcastCloudStatus()
|
|
5143
5170
|
const localSttActive = localSttFallbackCoordinator.isActive()
|
|
5144
5171
|
const now = Date.now()
|
|
5145
5172
|
if (now - this.lastSttQuotaNoticeAt >= STT_QUOTA_NOTICE_DEBOUNCE_MS) {
|
|
@@ -5383,7 +5410,7 @@ class LocalMiniappRuntime {
|
|
|
5383
5410
|
)
|
|
5384
5411
|
}
|
|
5385
5412
|
|
|
5386
|
-
private currentCloudStatus():
|
|
5413
|
+
private currentCloudStatus(): MiniappCloudStatus {
|
|
5387
5414
|
const base = cloudClientService.getStatus()
|
|
5388
5415
|
const fallbackActive =
|
|
5389
5416
|
(useSettingsStore.getState().getSetting(ISLAND_SETTINGS_KEYS.localSttFallbackActive) as boolean | undefined) ===
|
|
@@ -5394,6 +5421,7 @@ class LocalMiniappRuntime {
|
|
|
5394
5421
|
return {
|
|
5395
5422
|
status: base.status,
|
|
5396
5423
|
audioTransport: fallbackActive || sonioxDegraded ? "offline" : base.audioTransport,
|
|
5424
|
+
quotaBlocked: localSttFallbackCoordinator.isSttQuotaBlocked() ? ["transcription"] : [],
|
|
5397
5425
|
}
|
|
5398
5426
|
}
|
|
5399
5427
|
|
|
@@ -5543,7 +5571,7 @@ class LocalMiniappRuntime {
|
|
|
5543
5571
|
|
|
5544
5572
|
for (const packageName of matchedSubs) {
|
|
5545
5573
|
if (only !== undefined && packageName !== only) continue
|
|
5546
|
-
let transcriptionRoute:
|
|
5574
|
+
let transcriptionRoute: TranscriptionDeliveryRoute | undefined
|
|
5547
5575
|
if (normalizedStream.startsWith("transcription:") && transcriptionSource) {
|
|
5548
5576
|
const routes = this.appTranscriptionRoutesForEvent(packageName, normalizedStream)
|
|
5549
5577
|
transcriptionRoute =
|
|
@@ -5638,17 +5666,12 @@ class LocalMiniappRuntime {
|
|
|
5638
5666
|
})
|
|
5639
5667
|
}
|
|
5640
5668
|
|
|
5641
|
-
private appTranscriptionRoutesForSubscription(
|
|
5642
|
-
packageName: string,
|
|
5643
|
-
stream: string,
|
|
5644
|
-
): {
|
|
5645
|
-
cloud: boolean
|
|
5646
|
-
forceLocal: boolean
|
|
5647
|
-
} {
|
|
5669
|
+
private appTranscriptionRoutesForSubscription(packageName: string, stream: string): TranscriptionSubscriberRoutes {
|
|
5648
5670
|
const app = this.connectedApps.get(packageName)
|
|
5649
5671
|
return {
|
|
5650
5672
|
cloud: app?.cloudTranscriptionStreams.has(stream) === true,
|
|
5651
5673
|
forceLocal: app?.forceLocalTranscriptionStreams.has(stream) === true,
|
|
5674
|
+
localFallback: app?.noFallbackTranscriptionStreams.has(stream) !== true,
|
|
5652
5675
|
}
|
|
5653
5676
|
}
|
|
5654
5677
|
|
|
@@ -5666,21 +5689,20 @@ class LocalMiniappRuntime {
|
|
|
5666
5689
|
}
|
|
5667
5690
|
}
|
|
5668
5691
|
|
|
5669
|
-
private appTranscriptionRoutesForEvent(
|
|
5670
|
-
packageName: string,
|
|
5671
|
-
stream: string,
|
|
5672
|
-
): {
|
|
5673
|
-
cloud: boolean
|
|
5674
|
-
forceLocal: boolean
|
|
5675
|
-
} {
|
|
5692
|
+
private appTranscriptionRoutesForEvent(packageName: string, stream: string): TranscriptionSubscriberRoutes {
|
|
5676
5693
|
const app = this.connectedApps.get(packageName)
|
|
5677
5694
|
const autoStream = `${MiniappStreamType.TRANSCRIPTION}:auto`
|
|
5695
|
+
const cloudExact = app?.cloudTranscriptionStreams.has(stream) === true
|
|
5696
|
+
const cloudAuto = app?.cloudTranscriptionStreams.has(autoStream) === true
|
|
5678
5697
|
return {
|
|
5679
|
-
cloud:
|
|
5680
|
-
app?.cloudTranscriptionStreams.has(stream) === true || app?.cloudTranscriptionStreams.has(autoStream) === true,
|
|
5698
|
+
cloud: cloudExact || cloudAuto,
|
|
5681
5699
|
forceLocal:
|
|
5682
5700
|
app?.forceLocalTranscriptionStreams.has(stream) === true ||
|
|
5683
5701
|
app?.forceLocalTranscriptionStreams.has(autoStream) === true,
|
|
5702
|
+
// Fallback stays accepted while any matching default subscription takes it.
|
|
5703
|
+
localFallback:
|
|
5704
|
+
(cloudExact && app?.noFallbackTranscriptionStreams.has(stream) !== true) ||
|
|
5705
|
+
(cloudAuto && app?.noFallbackTranscriptionStreams.has(autoStream) !== true),
|
|
5684
5706
|
}
|
|
5685
5707
|
}
|
|
5686
5708
|
|
|
@@ -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
|
+
}
|