@mentra/cloud-client 3.2.0-dev.136 → 3.2.0-dev.153

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/cloud-client",
3
- "version": "3.2.0-dev.136",
3
+ "version": "3.2.0-dev.153",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
@@ -12,7 +12,7 @@
12
12
  "test": "bun test"
13
13
  },
14
14
  "dependencies": {
15
- "@mentra/cloud-protocol": "3.2.0-dev.136",
15
+ "@mentra/cloud-protocol": "3.2.0-dev.153",
16
16
  "tweetnacl": "^1.0.3"
17
17
  },
18
18
  "devDependencies": {
package/src/client.ts CHANGED
@@ -14,28 +14,28 @@
14
14
  *
15
15
  * See docs/issues/004-cloud-client/design.md ("The top-level CloudClient").
16
16
  */
17
- import { noopLogger } from "./logger";
18
- import type { Logger } from "./logger";
19
- import type { CloudClientConfig } from "./config";
20
- import { createHttpClient } from "./http";
21
- import { CloudClientError } from "./errors";
22
- import type { ConnectionInit } from "@mentra/cloud-protocol";
23
- import { systemTimers } from "./timers";
17
+ import {noopLogger} from "./logger"
18
+ import type {Logger} from "./logger"
19
+ import type {CloudClientConfig} from "./config"
20
+ import {createHttpClient} from "./http"
21
+ import {CloudClientError} from "./errors"
22
+ import type {ConnectionInit} from "@mentra/cloud-protocol"
23
+ import {systemTimers} from "./timers"
24
24
 
25
25
  // The module implementations. Each is owned by another agent under ./modules/**;
26
26
  // this file only constructs them, matching the constructor signatures fixed in
27
27
  // design.md exactly.
28
- import { Auth } from "./modules/auth/auth";
29
- import { TokenStore } from "./modules/auth/token-store";
30
- import { Runtime } from "./modules/runtime/runtime";
31
- import { Connection } from "./modules/runtime/connection";
32
- import { RuntimeEmitter } from "./modules/runtime/emitter";
33
- import { Subscriptions } from "./modules/runtime/subscriptions";
34
- import { Camera } from "./modules/runtime/camera";
35
- import { Maps } from "./modules/runtime/maps";
36
- import { Tts } from "./modules/runtime/tts";
37
- import { UdpAudio } from "./modules/runtime/audio-udp";
38
- import { Core } from "./modules/core/core";
28
+ import {Auth} from "./modules/auth/auth"
29
+ import {TokenStore} from "./modules/auth/token-store"
30
+ import {Runtime} from "./modules/runtime/runtime"
31
+ import {Connection} from "./modules/runtime/connection"
32
+ import {RuntimeEmitter} from "./modules/runtime/emitter"
33
+ import {Subscriptions} from "./modules/runtime/subscriptions"
34
+ import {Camera} from "./modules/runtime/camera"
35
+ import {Maps} from "./modules/runtime/maps"
36
+ import {Tts} from "./modules/runtime/tts"
37
+ import {UdpAudio} from "./modules/runtime/audio-udp"
38
+ import {Core} from "./modules/core/core"
39
39
 
40
40
  /**
41
41
  * Default reconnect/backoff for the live socket when a host supplies none.
@@ -47,7 +47,7 @@ import { Core } from "./modules/core/core";
47
47
  * lockstep after a shared blip. A host can override any of these through
48
48
  * `config.reconnect`.
49
49
  */
50
- const DEFAULT_RECONNECT = { baseMs: 500, maxMs: 5_000, jitter: true };
50
+ const DEFAULT_RECONNECT = {baseMs: 500, maxMs: 5_000, jitter: true}
51
51
 
52
52
  /**
53
53
  * The default audio codec the client announces in the handshake.
@@ -57,8 +57,8 @@ const DEFAULT_RECONNECT = { baseMs: 500, maxMs: 5_000, jitter: true };
57
57
  * now the handshake announces the device default so audio that starts immediately
58
58
  * after connect is decoded correctly.
59
59
  */
60
- const DEFAULT_AUDIO_CODEC = "pcm" as const;
61
- const DEFAULT_AUDIO_SAMPLE_RATE = 16_000;
60
+ const DEFAULT_AUDIO_CODEC = "pcm" as const
61
+ const DEFAULT_AUDIO_SAMPLE_RATE = 16_000
62
62
 
63
63
  /**
64
64
  * The protocol semver this client build speaks, announced in `connection.init`.
@@ -66,7 +66,7 @@ const DEFAULT_AUDIO_SAMPLE_RATE = 16_000;
66
66
  * Hardcoded to the 2.x line this package targets; bumped here when the client
67
67
  * starts speaking a newer protocol build, so there is one place to change it.
68
68
  */
69
- const PROTOCOL_VERSION = "2.0.0";
69
+ const PROTOCOL_VERSION = "2.0.0"
70
70
 
71
71
  /**
72
72
  * Rewrite a base URL to route through a proxy host while preserving its path.
@@ -77,12 +77,12 @@ const PROTOCOL_VERSION = "2.0.0";
77
77
  * core/runtime address that carries a path prefix is not lost when proxied.
78
78
  */
79
79
  function rewriteThroughProxy(target: string, proxy: string): string {
80
- const proxyUrl = new URL(proxy);
81
- const targetUrl = new URL(target);
80
+ const proxyUrl = new URL(proxy)
81
+ const targetUrl = new URL(target)
82
82
  // Keep the target's path/query, take the proxy's origin.
83
- targetUrl.protocol = proxyUrl.protocol;
84
- targetUrl.host = proxyUrl.host;
85
- return targetUrl.toString();
83
+ targetUrl.protocol = proxyUrl.protocol
84
+ targetUrl.host = proxyUrl.host
85
+ return targetUrl.toString()
86
86
  }
87
87
 
88
88
  /**
@@ -95,10 +95,10 @@ function rewriteThroughProxy(target: string, proxy: string): string {
95
95
  * place that knows the runtime's HTTP shape also derives its socket URL.
96
96
  */
97
97
  function toRuntimeWsUrl(httpBase: string): string {
98
- const u = new URL(httpBase);
99
- u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
100
- u.pathname = `${u.pathname.replace(/\/$/, "")}/ws/session`;
101
- return u.toString();
98
+ const u = new URL(httpBase)
99
+ u.protocol = u.protocol === "https:" ? "wss:" : "ws:"
100
+ u.pathname = `${u.pathname.replace(/\/$/, "")}/ws/session`
101
+ return u.toString()
102
102
  }
103
103
 
104
104
  export class CloudClient {
@@ -107,27 +107,27 @@ export class CloudClient {
107
107
  // of its public contract (per design.md), so a host gets the full, typed
108
108
  // surface (`cloud.auth.getRuntimeToken()`, etc.) straight off these fields with
109
109
  // no parallel interface to keep in sync.
110
- readonly auth: Auth;
111
- readonly runtime: Runtime;
112
- readonly core?: Core;
110
+ readonly auth: Auth
111
+ readonly runtime: Runtime
112
+ readonly core?: Core
113
113
 
114
114
  constructor(config: CloudClientConfig) {
115
115
  // One logger for the whole client, so a host routes every module's logs in
116
116
  // one place. Default to the silent no-op so we never print uninvited.
117
- const logger: Logger = config.logger ?? noopLogger;
117
+ const logger: Logger = config.logger ?? noopLogger
118
118
 
119
119
  // Reconnect/backoff lives here so the socket's timing is tuned in one spot.
120
- const reconnect = config.reconnect ?? DEFAULT_RECONNECT;
121
- const timers = config.timers ?? systemTimers;
120
+ const reconnect = config.reconnect ?? DEFAULT_RECONNECT
121
+ const timers = config.timers ?? systemTimers
122
122
 
123
123
  // Resolve the two base addresses. With a proxy set, both route through it;
124
124
  // without one, each module talks to its own service directly.
125
- const { core: coreBase, runtime: runtimeBase, proxy } = config.endpoints;
126
- const coreUrl = coreBase ? (proxy ? rewriteThroughProxy(coreBase, proxy) : coreBase) : undefined;
127
- const runtimeUrl = proxy ? rewriteThroughProxy(runtimeBase, proxy) : runtimeBase;
125
+ const {core: coreBase, runtime: runtimeBase, proxy} = config.endpoints
126
+ const coreUrl = coreBase ? (proxy ? rewriteThroughProxy(coreBase, proxy) : coreBase) : undefined
127
+ const runtimeUrl = proxy ? rewriteThroughProxy(runtimeBase, proxy) : runtimeBase
128
128
 
129
129
  if (config.auth.core && !coreUrl) {
130
- throw new CloudClientError("auth.core requires endpoints.core");
130
+ throw new CloudClientError("auth.core requires endpoints.core")
131
131
  }
132
132
 
133
133
  // Runtime auth is the mandatory half (Core is optional). Guard it before the
@@ -135,12 +135,12 @@ export class CloudClient {
135
135
  // (`{ subjectToken, subjectTokenType }`, no `runtime`) gets a clear
136
136
  // configuration error instead of an opaque `TypeError` from `"source" in undefined`.
137
137
  if (!config.auth.runtime) {
138
- throw new CloudClientError("auth.runtime is required (got a pre-split/flat auth config?)");
138
+ throw new CloudClientError("auth.runtime is required (got a pre-split/flat auth config?)")
139
139
  }
140
140
 
141
- const runtimeUsesCore = "source" in config.auth.runtime && config.auth.runtime.source === "core";
141
+ const runtimeUsesCore = "source" in config.auth.runtime && config.auth.runtime.source === "core"
142
142
  if (runtimeUsesCore && (!coreUrl || !config.auth.core)) {
143
- throw new CloudClientError("auth.runtime.source='core' requires endpoints.core and auth.core");
143
+ throw new CloudClientError("auth.runtime.source='core' requires endpoints.core and auth.core")
144
144
  }
145
145
 
146
146
  // Build auth FIRST: runtime and core both source their Bearer from it, so it
@@ -151,9 +151,12 @@ export class CloudClient {
151
151
  // before any access token exists. It is deliberately Core-only: runtime-only
152
152
  // clients never get a fallback that points Core/Auth calls at Runtime.
153
153
  const authHttp = coreUrl
154
- ? createHttpClient({ baseUrl: coreUrl, logger, fetch: config.transports.http, timers })
155
- : undefined;
156
- const store = new TokenStore({ storage: config.transports.storage });
154
+ ? createHttpClient({baseUrl: coreUrl, logger, fetch: config.transports.http, timers})
155
+ : undefined
156
+ const store = new TokenStore({
157
+ storage: config.transports.storage,
158
+ storageKey: config.authStorageKey,
159
+ })
157
160
  const auth = new Auth({
158
161
  http: authHttp,
159
162
  store,
@@ -163,10 +166,11 @@ export class CloudClient {
163
166
  // requests, but still use the host's injected HTTP transport.
164
167
  baseUrl: coreUrl,
165
168
  fetch: config.transports.http,
166
- });
169
+ timers,
170
+ })
167
171
 
168
- const getRuntimeToken = (): Promise<string> => auth.getRuntimeToken();
169
- const getCoreToken = (): Promise<string> => auth.getCoreToken();
172
+ const getRuntimeToken = (): Promise<string> => auth.getRuntimeToken()
173
+ const getCoreToken = (): Promise<string> => auth.getCoreToken()
170
174
 
171
175
  const coreHttp =
172
176
  coreUrl && config.auth.core
@@ -177,17 +181,17 @@ export class CloudClient {
177
181
  fetch: config.transports.http,
178
182
  timers,
179
183
  })
180
- : null;
184
+ : null
181
185
  const runtimeHttp = createHttpClient({
182
186
  baseUrl: runtimeUrl,
183
187
  getToken: getRuntimeToken,
184
188
  logger,
185
189
  fetch: config.transports.http,
186
190
  timers,
187
- });
191
+ })
188
192
 
189
- const emitter = new RuntimeEmitter();
190
- const subscriptions = new Subscriptions({ http: runtimeHttp, timers });
193
+ const emitter = new RuntimeEmitter()
194
+ const subscriptions = new Subscriptions({http: runtimeHttp, timers})
191
195
 
192
196
  // The handshake payload the connection sends on every (re)open. It is a
193
197
  // factory (not a fixed value) so each reconnect re-reads the current defaults
@@ -213,10 +217,10 @@ export class CloudClient {
213
217
  sampleRate: config.audio?.sampleRate ?? DEFAULT_AUDIO_SAMPLE_RATE,
214
218
  // Only LC3 carries a frame size; the config type forces LC3 hosts to
215
219
  // state theirs explicitly (decoder is sized from this — no safe guess).
216
- ...(config.audio?.codec === "lc3" ? { frameSizeBytes: config.audio.frameSizeBytes } : {}),
220
+ ...(config.audio?.codec === "lc3" ? {frameSizeBytes: config.audio.frameSizeBytes} : {}),
217
221
  initialSubscriptions: subscriptions.currentSet(),
218
222
  },
219
- });
223
+ })
220
224
 
221
225
  // Build the remaining runtime pieces, then the runtime that orchestrates them.
222
226
  const connection = new Connection({
@@ -227,14 +231,14 @@ export class CloudClient {
227
231
  reconnect,
228
232
  timers,
229
233
  onAuthRejected: async () => {
230
- await auth.getRuntimeToken({ forceRefresh: true });
234
+ await auth.getRuntimeToken({forceRefresh: true})
231
235
  },
232
236
  logger,
233
- });
234
- const camera = new Camera({ http: runtimeHttp, timers });
235
- const tts = new Tts({ http: runtimeHttp });
236
- const maps = new Maps({ http: runtimeHttp });
237
- const audio = new UdpAudio({ udp: config.transports.udp });
237
+ })
238
+ const camera = new Camera({http: runtimeHttp, timers})
239
+ const tts = new Tts({http: runtimeHttp})
240
+ const maps = new Maps({http: runtimeHttp})
241
+ const audio = new UdpAudio({udp: config.transports.udp})
238
242
 
239
243
  const runtime = new Runtime({
240
244
  connection,
@@ -249,14 +253,14 @@ export class CloudClient {
249
253
  // On a fatal AUTH_EXPIRED at handshake, runtime forces auth to drop its
250
254
  // cached access token and refresh; the connection then re-reads the fresh
251
255
  // token via getRuntimeToken on the reopen.
252
- forceRefreshToken: () => auth.getRuntimeToken({ forceRefresh: true }),
253
- });
256
+ forceRefreshToken: () => auth.getRuntimeToken({forceRefresh: true}),
257
+ })
254
258
 
255
259
  // Core is last: stateless REST on the core service, Bearer from auth.
256
- const core = coreHttp ? new Core({ http: coreHttp }) : undefined;
260
+ const core = coreHttp ? new Core({http: coreHttp}) : undefined
257
261
 
258
- this.auth = auth;
259
- this.runtime = runtime;
260
- this.core = core;
262
+ this.auth = auth
263
+ this.runtime = runtime
264
+ this.core = core
261
265
  }
262
266
  }
package/src/config.ts CHANGED
@@ -7,9 +7,9 @@
7
7
  *
8
8
  * See docs/issues/004-cloud-client/spec.md ("Construction") and design.md.
9
9
  */
10
- import type { Logger } from "./logger";
11
- import type { CloudClientTransports } from "./transports";
12
- import type { CloudClientTimers } from "./timers";
10
+ import type {Logger} from "./logger"
11
+ import type {CloudClientTransports} from "./transports"
12
+ import type {CloudClientTimers} from "./timers"
13
13
 
14
14
  /**
15
15
  * The full shape passed to the root `CloudClient`.
@@ -23,28 +23,28 @@ export interface CloudClientConfig {
23
23
  // `core` is optional only for runtime-only deployments. If `auth.core` is set,
24
24
  // or if `auth.runtime.source` is `"core"`, this must be present; Core/Auth
25
25
  // calls are never routed to Runtime.
26
- endpoints: { core?: string; runtime: string; proxy?: string };
27
- auth: AuthConfig;
28
- transports: CloudClientTransports;
26
+ endpoints: {core?: string; runtime: string; proxy?: string}
27
+ auth: AuthConfig
28
+ transports: CloudClientTransports
29
29
  /**
30
30
  * Scheduler for all delayed client work, including connection/audio
31
31
  * liveness, retries, and request timeouts. React Native hosts must provide
32
32
  * native background timers because ordinary JS timers pause when Android
33
33
  * backgrounds the app.
34
34
  */
35
- timers?: CloudClientTimers;
36
- logger?: Logger;
35
+ timers?: CloudClientTimers
36
+ logger?: Logger
37
+ /** Override the persisted refresh-token key to isolate deployment sessions. */
38
+ authStorageKey?: string
37
39
  // backoff tuning for the live socket; one place so a host can match its fleet
38
- reconnect?: { baseMs: number; maxMs: number; jitter: boolean };
40
+ reconnect?: {baseMs: number; maxMs: number; jitter: boolean}
39
41
  /**
40
42
  * Audio format announced in `connection.init`. Defaults to PCM at 16 kHz when
41
43
  * omitted. An LC3 host MUST pass the frame size its encoder emits — the
42
44
  * runtime sizes its decoder from this field, and phone builds legitimately
43
45
  * differ (20/40/60); there is no safe default, so the type requires it.
44
46
  */
45
- audio?:
46
- | { codec: "pcm"; sampleRate?: number }
47
- | { codec: "lc3"; sampleRate?: number; frameSizeBytes: 20 | 40 | 60 };
47
+ audio?: {codec: "pcm"; sampleRate?: number} | {codec: "lc3"; sampleRate?: number; frameSizeBytes: 20 | 40 | 60}
48
48
  }
49
49
 
50
50
  /**
@@ -53,7 +53,7 @@ export interface CloudClientConfig {
53
53
  * The cloud's `/exchange` endpoint needs to know how to verify the incoming
54
54
  * token, so the type travels alongside the token itself.
55
55
  */
56
- export type SubjectTokenType = "oem-jwt" | "mentra-core" | "supabase";
56
+ export type SubjectTokenType = "oidc" | "oem-jwt" | "mentra-core" | "supabase"
57
57
 
58
58
  /**
59
59
  * The three ways a host can give the client its credentials.
@@ -65,11 +65,11 @@ export type SubjectTokenType = "oem-jwt" | "mentra-core" | "supabase";
65
65
  */
66
66
  export type CoreAuthConfig =
67
67
  // exchanged once on first use
68
- | { subjectToken: string; subjectTokenType: SubjectTokenType }
68
+ | {subjectToken: string; subjectTokenType: SubjectTokenType}
69
69
  // fetched on demand, for subject tokens that expire before exchange
70
- | { getSubjectToken: () => Promise<{ token: string; type: SubjectTokenType }> }
70
+ | {getSubjectToken: () => Promise<{token: string; type: SubjectTokenType}>}
71
71
  // already exchanged, skip straight to refresh
72
- | { accessToken: string; refreshToken: string };
72
+ | {accessToken: string; refreshToken: string}
73
73
 
74
74
  export type RuntimeAuthConfig =
75
75
  | {
@@ -77,16 +77,16 @@ export type RuntimeAuthConfig =
77
77
  * Ask Cloud Core/Auth to mint a short-lived `cloud-runtime` token. This is
78
78
  * explicit hosted-Core mode, not an implicit Core-token fallback.
79
79
  */
80
- source: "core";
80
+ source: "core"
81
81
  }
82
82
  | {
83
83
  /** Host/OEM/local-dev supplied runtime-token provider. */
84
- getToken(opts?: { forceRefresh?: boolean }): Promise<string>;
85
- };
84
+ getToken(opts?: {forceRefresh?: boolean}): Promise<string>
85
+ }
86
86
 
87
87
  export interface AuthConfig {
88
88
  // Core-backed auth owns identity, Core token exchange/refresh, miniapp token
89
89
  // minting, and miniapp auto-auth. Omit only for true runtime-only deployments.
90
- core?: CoreAuthConfig;
91
- runtime: RuntimeAuthConfig;
90
+ core?: CoreAuthConfig
91
+ runtime: RuntimeAuthConfig
92
92
  }
package/src/errors.ts CHANGED
@@ -40,6 +40,24 @@ export class HttpError extends CloudClientError {
40
40
  }
41
41
  }
42
42
 
43
+ /**
44
+ * Thrown by `cloud.auth.clearSession()` when local credentials were cleared but
45
+ * Core did not confirm revocation of the session (revoke failed after bounded
46
+ * retries, or the token needed to authorize it could not be refreshed). The
47
+ * client is logged out locally; the host must not treat the server-side session
48
+ * as dead and should surface or defer the failure. `cause` is the underlying error.
49
+ */
50
+ export class SessionRevocationError extends CloudClientError {
51
+ readonly cause: unknown;
52
+
53
+ constructor(message: string, options?: { cause?: unknown }) {
54
+ super(message);
55
+ this.name = "SessionRevocationError";
56
+ this.cause = options?.cause;
57
+ Object.setPrototypeOf(this, new.target.prototype);
58
+ }
59
+ }
60
+
43
61
  /**
44
62
  * Thrown when a token refresh fails and the host must send the user back through
45
63
  * login. Separate from `HttpError` so a host can catch the "credentials are
package/src/index.ts CHANGED
@@ -17,6 +17,7 @@
17
17
  // The top-level object. Implemented in ./client by the client agent; re-exported
18
18
  // here so the public import is `@mentra/cloud-client`, not a deep path.
19
19
  export { CloudClient } from "./client";
20
+ export { DEFAULT_REFRESH_TOKEN_KEY } from "./modules/auth/token-store";
20
21
 
21
22
  // Construction contract.
22
23
  export type {
@@ -35,7 +36,12 @@ export type {
35
36
  export type { CloudClientTimers } from "./timers";
36
37
 
37
38
  // Local error types a host can branch on with `instanceof`.
38
- export { CloudClientError, HttpError, AuthExpiredError } from "./errors";
39
+ export {
40
+ CloudClientError,
41
+ HttpError,
42
+ AuthExpiredError,
43
+ SessionRevocationError,
44
+ } from "./errors";
39
45
 
40
46
  // The logging hook a host can implement to route library logs.
41
47
  export { noopLogger } from "./logger";