@glassly/engine 0.3.2-dev.586 → 0.3.2-dev.596

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/build/engine.d.ts +14 -0
  2. package/build/engine.d.ts.map +1 -1
  3. package/build/engine.js +4 -0
  4. package/build/engine.js.map +1 -1
  5. package/build/facades/glasses.d.ts +20 -0
  6. package/build/facades/glasses.d.ts.map +1 -1
  7. package/build/facades/glasses.js +21 -0
  8. package/build/facades/glasses.js.map +1 -1
  9. package/build/facades/pairing.js +1 -1
  10. package/build/facades/pairing.js.map +1 -1
  11. package/build/generated/releaseMetadata.js +3 -3
  12. package/build/generated/releaseMetadata.js.map +1 -1
  13. package/build/index.d.ts +2 -0
  14. package/build/index.d.ts.map +1 -1
  15. package/build/index.js +2 -0
  16. package/build/index.js.map +1 -1
  17. package/build/services/DeviceEventRouter.d.ts.map +1 -1
  18. package/build/services/DeviceEventRouter.js +5 -0
  19. package/build/services/DeviceEventRouter.js.map +1 -1
  20. package/build/services/GestureDotsService.d.ts +9 -9
  21. package/build/services/GestureDotsService.d.ts.map +1 -1
  22. package/build/services/GestureDotsService.js +75 -50
  23. package/build/services/GestureDotsService.js.map +1 -1
  24. package/build/services/GlassesTouchInput.d.ts +1 -1
  25. package/build/services/GlassesTouchInput.d.ts.map +1 -1
  26. package/build/services/GlassesTouchInput.js +7 -4
  27. package/build/services/GlassesTouchInput.js.map +1 -1
  28. package/build/services/gestureDotGlyphs.d.ts +1 -1
  29. package/build/services/gestureDotGlyphs.d.ts.map +1 -1
  30. package/build/services/gestureDotGlyphs.js +5 -0
  31. package/build/services/gestureDotGlyphs.js.map +1 -1
  32. package/build/services/neuralBand/MetaAuth.d.ts +31 -0
  33. package/build/services/neuralBand/MetaAuth.d.ts.map +1 -0
  34. package/build/services/neuralBand/MetaAuth.js +161 -0
  35. package/build/services/neuralBand/MetaAuth.js.map +1 -0
  36. package/build/services/neuralBand/NeuralBandInput.d.ts +50 -0
  37. package/build/services/neuralBand/NeuralBandInput.d.ts.map +1 -0
  38. package/build/services/neuralBand/NeuralBandInput.js +114 -0
  39. package/build/services/neuralBand/NeuralBandInput.js.map +1 -0
  40. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts +3 -0
  41. package/build/services/neuralBand/NeuralBandSettingsSync.d.ts.map +1 -0
  42. package/build/services/neuralBand/NeuralBandSettingsSync.js +52 -0
  43. package/build/services/neuralBand/NeuralBandSettingsSync.js.map +1 -0
  44. package/build/services/neuralBand/sha256.d.ts +7 -0
  45. package/build/services/neuralBand/sha256.d.ts.map +1 -0
  46. package/build/services/neuralBand/sha256.js +68 -0
  47. package/build/services/neuralBand/sha256.js.map +1 -0
  48. package/build/stores/settings.d.ts.map +1 -1
  49. package/build/stores/settings.js +18 -0
  50. package/build/stores/settings.js.map +1 -1
  51. package/build/types/enums.d.ts +2 -1
  52. package/build/types/enums.d.ts.map +1 -1
  53. package/build/types/enums.js +1 -0
  54. package/build/types/enums.js.map +1 -1
  55. package/package.json +7 -7
  56. package/src/engine.ts +4 -0
  57. package/src/facades/glasses.ts +27 -0
  58. package/src/facades/pairing.ts +1 -1
  59. package/src/generated/releaseMetadata.ts +3 -3
  60. package/src/index.ts +14 -0
  61. package/src/services/DeviceEventRouter.ts +5 -0
  62. package/src/services/GestureDotsService.ts +72 -49
  63. package/src/services/GlassesTouchInput.ts +13 -5
  64. package/src/services/gestureDotGlyphs.ts +6 -1
  65. package/src/services/neuralBand/MetaAuth.ts +192 -0
  66. package/src/services/neuralBand/NeuralBandInput.ts +148 -0
  67. package/src/services/neuralBand/NeuralBandSettingsSync.ts +63 -0
  68. package/src/services/neuralBand/sha256.ts +70 -0
  69. package/src/stores/settings.ts +18 -0
  70. package/src/types/enums.ts +1 -0
@@ -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
+ }
@@ -0,0 +1,148 @@
1
+ /**
2
+ * The Meta Neural Band's own event streams, past the gestures that ride `touch_event`:
3
+ * claim progress (for the pairing screens), the handwriting model (typed into the
4
+ * miniapp keyboard stream as synthesized key presses) and the air mouse (forwarded
5
+ * to miniapps on the pointer stream).
6
+ */
7
+ import type {NeuralBandClaimEvent, NeuralBandHandwritingEvent, NeuralBandPointerEvent} from "@glassly/bluetooth-sdk"
8
+ import {MiniappStreamType} from "@glassly/miniapp"
9
+
10
+ import type {KeyboardEventPayload} from "../GlassesKeyboardInput"
11
+ import localMiniappRuntime from "../LocalMiniappRuntime"
12
+
13
+ export interface NeuralBandClaimStatus {
14
+ stage: "idle" | "progress" | "completed" | "failed"
15
+ message?: string
16
+ sessionExpired: boolean
17
+ bandName?: string
18
+ }
19
+
20
+ export interface NeuralBandHandwritingStatus {
21
+ phase: "idle" | "preparing" | "ready" | "restoring" | "finished"
22
+ message: string
23
+ problem?: string
24
+ /** The last text the band produced since handwriting was switched on. */
25
+ text: string
26
+ }
27
+
28
+ export interface NeuralBandInputOptions {
29
+ forward?: (stream: string, payload: unknown) => void
30
+ now?: () => number
31
+ }
32
+
33
+ const MODIFIERS = {shift: false, ctrl: false, alt: false, meta: false}
34
+
35
+ /** One handwriting change as the key presses that would have typed it. */
36
+ export function keyEventsForTextChange(previous: string, next: string, timestamp: number): KeyboardEventPayload[] {
37
+ let common = 0
38
+ while (common < previous.length && common < next.length && previous[common] === next[common]) common++
39
+ const events: KeyboardEventPayload[] = []
40
+ const press = (key: string, code: string, text?: string) => {
41
+ events.push({phase: "down", key, code, modifiers: MODIFIERS, text, repeat: false, timestamp})
42
+ events.push({phase: "up", key, code, modifiers: MODIFIERS, repeat: false, timestamp})
43
+ }
44
+ for (let i = common; i < previous.length; i++) press("Backspace", "Backspace")
45
+ for (const char of Array.from(next.slice(common))) {
46
+ if (char === "\n") press("Enter", "Enter")
47
+ else if (char === " ") press(" ", "Space", " ")
48
+ else press(char, codeFor(char), char)
49
+ }
50
+ return events
51
+ }
52
+
53
+ function codeFor(char: string): string {
54
+ if (/^[a-z]$/i.test(char)) return `Key${char.toUpperCase()}`
55
+ if (/^[0-9]$/.test(char)) return `Digit${char}`
56
+ return "Unidentified"
57
+ }
58
+
59
+ export class NeuralBandInput {
60
+ private claim: NeuralBandClaimStatus = {stage: "idle", sessionExpired: false}
61
+ private handwriting: NeuralBandHandwritingStatus = {phase: "idle", message: "", text: ""}
62
+ private claimListeners = new Set<(status: NeuralBandClaimStatus) => void>()
63
+ private handwritingListeners = new Set<(status: NeuralBandHandwritingStatus) => void>()
64
+ private readonly forward: (stream: string, payload: unknown) => void
65
+ private readonly now: () => number
66
+
67
+ constructor(options: NeuralBandInputOptions = {}) {
68
+ this.forward = options.forward ?? ((stream, payload) => localMiniappRuntime.forwardEvent(stream, payload))
69
+ this.now = options.now ?? (() => Date.now())
70
+ }
71
+
72
+ claimStatus(): NeuralBandClaimStatus {
73
+ return this.claim
74
+ }
75
+
76
+ onClaimStatus(cb: (status: NeuralBandClaimStatus) => void): () => void {
77
+ this.claimListeners.add(cb)
78
+ return () => this.claimListeners.delete(cb)
79
+ }
80
+
81
+ handwritingStatus(): NeuralBandHandwritingStatus {
82
+ return this.handwriting
83
+ }
84
+
85
+ onHandwritingStatus(cb: (status: NeuralBandHandwritingStatus) => void): () => void {
86
+ this.handwritingListeners.add(cb)
87
+ return () => this.handwritingListeners.delete(cb)
88
+ }
89
+
90
+ /** A new pairing attempt starts from a clean slate. */
91
+ resetClaim(): void {
92
+ this.setClaim({stage: "idle", sessionExpired: false})
93
+ }
94
+
95
+ handleClaim(event: NeuralBandClaimEvent): void {
96
+ this.setClaim({
97
+ stage: event.stage,
98
+ message: event.message,
99
+ sessionExpired: event.sessionExpired === true,
100
+ bandName: event.bandName,
101
+ })
102
+ }
103
+
104
+ handleHandwriting(event: NeuralBandHandwritingEvent): void {
105
+ if (event.kind === "state") {
106
+ const text = event.phase === "preparing" ? "" : this.handwriting.text
107
+ this.setHandwriting({phase: event.phase, message: event.message, problem: event.problem, text})
108
+ return
109
+ }
110
+ const previous = this.handwriting.text
111
+ if (event.text !== previous) {
112
+ for (const key of keyEventsForTextChange(previous, event.text, this.now())) {
113
+ this.forward(MiniappStreamType.KEYBOARD_EVENT, key)
114
+ }
115
+ }
116
+ this.setHandwriting({...this.handwriting, text: event.text})
117
+ }
118
+
119
+ handlePointer(event: NeuralBandPointerEvent): void {
120
+ if (event.kind === "move") {
121
+ this.forward(MiniappStreamType.POINTER_EVENT, {
122
+ kind: "move",
123
+ dx: event.dx,
124
+ dy: event.dy,
125
+ timestamp: event.timestamp,
126
+ })
127
+ } else {
128
+ this.forward(MiniappStreamType.POINTER_EVENT, {
129
+ kind: "buttons",
130
+ buttons: event.buttons,
131
+ timestamp: event.timestamp,
132
+ })
133
+ }
134
+ }
135
+
136
+ private setClaim(status: NeuralBandClaimStatus): void {
137
+ this.claim = status
138
+ for (const cb of this.claimListeners) cb(status)
139
+ }
140
+
141
+ private setHandwriting(status: NeuralBandHandwritingStatus): void {
142
+ this.handwriting = status
143
+ for (const cb of this.handwritingListeners) cb(status)
144
+ }
145
+ }
146
+
147
+ const neuralBandInput = new NeuralBandInput()
148
+ export default neuralBandInput
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Pushes the Neural Band's user settings (air mouse, wearing hand) to the native
3
+ * controller: when the band connects, and whenever either setting changes while
4
+ * it is connected. The native side keeps nothing across connections.
5
+ */
6
+ import BluetoothSdk from "@glassly/bluetooth-sdk/internal"
7
+
8
+ import {useGlassesStore} from "../../stores/glasses"
9
+ import {SETTINGS, useSettingsStore} from "../../stores/settings"
10
+ import {ControllerTypes} from "../../types/enums"
11
+
12
+ let unsubscribers: (() => void)[] = []
13
+
14
+ function bandConnected(): boolean {
15
+ const glasses = useGlassesStore.getState()
16
+ const settings = useSettingsStore.getState()
17
+ return (
18
+ glasses.controllerConnected === true &&
19
+ glasses.controllerFullyBooted === true &&
20
+ settings.getSetting(SETTINGS.default_controller.key) === ControllerTypes.NEURAL_BAND
21
+ )
22
+ }
23
+
24
+ function pushAirMouse(): void {
25
+ if (!bandConnected()) return
26
+ const enabled = useSettingsStore.getState().getSetting(SETTINGS.neural_band_air_mouse.key) === true
27
+ void BluetoothSdk.neuralBandSetPointer(enabled).catch((err) =>
28
+ console.warn("NeuralBandSettingsSync: air mouse push failed:", err),
29
+ )
30
+ }
31
+
32
+ function pushHand(): void {
33
+ if (!bandConnected()) return
34
+ const hand = useSettingsStore.getState().getSetting(SETTINGS.neural_band_hand.key) === "left" ? "left" : "right"
35
+ void BluetoothSdk.neuralBandSetHand(hand).catch((err) =>
36
+ console.warn("NeuralBandSettingsSync: hand push failed:", err),
37
+ )
38
+ }
39
+
40
+ export function startNeuralBandSettingsSync(): void {
41
+ if (unsubscribers.length > 0) return
42
+ let wasConnected = bandConnected()
43
+ unsubscribers = [
44
+ useGlassesStore.subscribe(
45
+ (s) => s.controllerConnected === true && s.controllerFullyBooted === true,
46
+ () => {
47
+ const connected = bandConnected()
48
+ if (connected && !wasConnected) {
49
+ pushAirMouse()
50
+ pushHand()
51
+ }
52
+ wasConnected = connected
53
+ },
54
+ ),
55
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.neural_band_air_mouse.key), pushAirMouse),
56
+ useSettingsStore.subscribe((s) => s.getSetting(SETTINGS.neural_band_hand.key), pushHand),
57
+ ]
58
+ }
59
+
60
+ export function stopNeuralBandSettingsSync(): void {
61
+ for (const unsubscribe of unsubscribers) unsubscribe()
62
+ unsubscribers = []
63
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * SHA-256 over a UTF-8 string, hex encoded. The Meta sign-in callback carries the
3
+ * first 16 hex digits of the request token's digest; nothing else here needs a
4
+ * hash, and the app ships no crypto dependency, so this stays small and local.
5
+ */
6
+
7
+ const K = new Uint32Array([
8
+ 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, 0xd807aa98,
9
+ 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
10
+ 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, 0x983e5152, 0xa831c66d, 0xb00327c8,
11
+ 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
12
+ 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819,
13
+ 0xd6990624, 0xf40e3585, 0x106aa070, 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
14
+ 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7,
15
+ 0xc67178f2,
16
+ ])
17
+
18
+ function rotr(x: number, n: number): number {
19
+ return (x >>> n) | (x << (32 - n))
20
+ }
21
+
22
+ export function sha256Hex(text: string): string {
23
+ const bytes = new TextEncoder().encode(text)
24
+ const bitLength = bytes.length * 8
25
+ const padded = new Uint8Array(Math.ceil((bytes.length + 9) / 64) * 64)
26
+ padded.set(bytes)
27
+ padded[bytes.length] = 0x80
28
+ const view = new DataView(padded.buffer)
29
+ view.setUint32(padded.length - 8, Math.floor(bitLength / 0x100000000))
30
+ view.setUint32(padded.length - 4, bitLength >>> 0)
31
+
32
+ const h = new Uint32Array([
33
+ 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19,
34
+ ])
35
+ const w = new Uint32Array(64)
36
+ for (let offset = 0; offset < padded.length; offset += 64) {
37
+ for (let i = 0; i < 16; i++) w[i] = view.getUint32(offset + i * 4)
38
+ for (let i = 16; i < 64; i++) {
39
+ const s0 = rotr(w[i - 15], 7) ^ rotr(w[i - 15], 18) ^ (w[i - 15] >>> 3)
40
+ const s1 = rotr(w[i - 2], 17) ^ rotr(w[i - 2], 19) ^ (w[i - 2] >>> 10)
41
+ w[i] = (w[i - 16] + s0 + w[i - 7] + s1) >>> 0
42
+ }
43
+ let [a, b, c, d, e, f, g, hh] = h
44
+ for (let i = 0; i < 64; i++) {
45
+ const s1 = rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)
46
+ const ch = (e & f) ^ (~e & g)
47
+ const t1 = (hh + s1 + ch + K[i] + w[i]) >>> 0
48
+ const s0 = rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)
49
+ const maj = (a & b) ^ (a & c) ^ (b & c)
50
+ const t2 = (s0 + maj) >>> 0
51
+ hh = g
52
+ g = f
53
+ f = e
54
+ e = (d + t1) >>> 0
55
+ d = c
56
+ c = b
57
+ b = a
58
+ a = (t1 + t2) >>> 0
59
+ }
60
+ h[0] = (h[0] + a) >>> 0
61
+ h[1] = (h[1] + b) >>> 0
62
+ h[2] = (h[2] + c) >>> 0
63
+ h[3] = (h[3] + d) >>> 0
64
+ h[4] = (h[4] + e) >>> 0
65
+ h[5] = (h[5] + f) >>> 0
66
+ h[6] = (h[6] + g) >>> 0
67
+ h[7] = (h[7] + hh) >>> 0
68
+ }
69
+ return Array.from(h, (word) => word.toString(16).padStart(8, "0")).join("")
70
+ }
@@ -755,6 +755,24 @@ export const SETTINGS: Record<string, Setting> = {
755
755
  saveOnServer: false,
756
756
  persist: true,
757
757
  },
758
+ // Meta Neural Band: the air mouse streams the band's orientation at 128 Hz, so it
759
+ // stays off unless asked for. The band side is pushed when the band connects.
760
+ neural_band_air_mouse: {
761
+ key: "neural_band_air_mouse",
762
+ defaultValue: () => false,
763
+ writable: true,
764
+ saveOnServer: false,
765
+ persist: true,
766
+ },
767
+ // Which wrist wears the band: flips the air mouse's forearm axis and is written
768
+ // to the band's own hand setting on connect.
769
+ neural_band_hand: {
770
+ key: "neural_band_hand",
771
+ defaultValue: () => "right",
772
+ writable: true,
773
+ saveOnServer: false,
774
+ persist: true,
775
+ },
758
776
  // Maximum rate at which display updates leave the phone for the glasses.
759
777
  // The Bluetooth SDK paces every scene frame and legacy layout to this,
760
778
  // coalescing faster updates last-wins; clears bypass it. No effect on G2
@@ -17,4 +17,5 @@ export enum DeviceTypes {
17
17
 
18
18
  export enum ControllerTypes {
19
19
  R1 = "Even Realities R1",
20
+ NEURAL_BAND = "Meta Neural Band",
20
21
  }