@mentra/cloud-protocol 3.4.0-dev.1071 → 3.4.0-dev.1074

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/fleet.ts +143 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mentra/cloud-protocol",
3
- "version": "3.4.0-dev.1071",
3
+ "version": "3.4.0-dev.1074",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
package/src/fleet.ts CHANGED
@@ -17,6 +17,8 @@ export const FLEET_LIMITS = {
17
17
  maxBatchBytes: 1_048_576,
18
18
  maxSessionsPerCheckpoint: 32,
19
19
  maxSessionElapsedMs: 7 * 24 * 60 * 60 * 1000,
20
+ maxInventoryMiniapps: 500,
21
+ maxAssignmentEntries: 500,
20
22
  phoneQueueMaxRecords: 10_000,
21
23
  phoneQueueMaxAgeMs: 7 * 24 * 60 * 60 * 1000,
22
24
  replayWindowMs: 14 * 24 * 60 * 60 * 1000,
@@ -60,6 +62,7 @@ export const FLEET_CAPABILITIES = [
60
62
  "app_foreground",
61
63
  "engine_active",
62
64
  "photo_operations",
65
+ "phone_battery",
63
66
  ] as const
64
67
 
65
68
  const Battery = z.discriminatedUnion("state", [
@@ -86,6 +89,8 @@ export const FleetSnapshotPayloadSchema = z
86
89
  appBuild: FleetText,
87
90
  engineVersion: FleetText.optional(),
88
91
  sdkVersion: FleetText.optional(),
92
+ /** The phone's own battery; absent from an app that does not report one. */
93
+ battery: Battery.optional(),
89
94
  })
90
95
  .strict(),
91
96
  glasses: z
@@ -154,6 +159,96 @@ export const FleetPhotoPayloadSchema = z
154
159
  .strict()
155
160
  .refine((v) => (v.stage === "failed") === (v.failedStage !== undefined), "failedStage goes with stage failed")
156
161
 
162
+ /** The provisioning spec's installation reasons. Management reasons are kept apart from the artifact's provenance. */
163
+ export const FLEET_INSTALL_REASONS = ["bundled", "explicit", "manifest", "assignment"] as const
164
+ export type FleetInstallReason = (typeof FLEET_INSTALL_REASONS)[number]
165
+
166
+ /** Phase 11: the phone's installed miniapps. Sent when the collector starts and whenever the inventory changes. */
167
+ export const FleetMiniappInventoryPayloadSchema = z
168
+ .object({
169
+ miniapps: z
170
+ .array(
171
+ z
172
+ .object({
173
+ packageName: FleetText,
174
+ versionName: FleetText,
175
+ digest: FleetText,
176
+ reasons: z.array(z.enum(FLEET_INSTALL_REASONS)).min(1).max(FLEET_INSTALL_REASONS.length),
177
+ })
178
+ .strict(),
179
+ )
180
+ .max(FLEET_LIMITS.maxInventoryMiniapps),
181
+ })
182
+ .strict()
183
+
184
+ /** The provisioning spec's outcome states for one assignment on one phone. */
185
+ export const FLEET_ASSIGNMENT_STATES = [
186
+ "pending",
187
+ "deferred_running",
188
+ "downloading",
189
+ "installing",
190
+ "installed",
191
+ "blocked",
192
+ "retrying",
193
+ "withdrawn",
194
+ ] as const
195
+ export type FleetAssignmentState = (typeof FLEET_ASSIGNMENT_STATES)[number]
196
+
197
+ /** Stable reasons for a blocked or retrying assignment. Never error text. */
198
+ export const FLEET_ASSIGNMENT_REASONS = [
199
+ "archived",
200
+ "access_denied",
201
+ "not_a_miniapp",
202
+ "download_failed",
203
+ "signature_invalid",
204
+ "incompatible_host",
205
+ "newer_version_installed",
206
+ "manifest_conflict",
207
+ "storage_full",
208
+ ] as const
209
+ export type FleetAssignmentReason = (typeof FLEET_ASSIGNMENT_REASONS)[number]
210
+
211
+ /** A miniapp's address: https, bounded, and with no credentials in it. */
212
+ export const FleetMiniappUrl = z
213
+ .string()
214
+ .min(1)
215
+ .max(2048)
216
+ .refine((value) => {
217
+ try {
218
+ const url = new URL(value)
219
+ return url.protocol === "https:" && url.username === "" && url.password === ""
220
+ } catch {
221
+ return false
222
+ }
223
+ }, "a miniapp URL is an https address without credentials")
224
+
225
+ const FleetRevision = z.number().int().min(1).max(Number.MAX_SAFE_INTEGER)
226
+
227
+ /** Phase 11: one assignment's state on this phone. */
228
+ export const FleetAssignmentOutcomePayloadSchema = z
229
+ .object({
230
+ assignmentId: FleetId,
231
+ /** The assignment's own revision. */
232
+ assignmentRevision: FleetRevision,
233
+ /** The account's desired-state revision the phone acted on. */
234
+ desiredRevision: FleetRevision,
235
+ packageName: FleetText,
236
+ state: z.enum(FLEET_ASSIGNMENT_STATES),
237
+ reason: z.enum(FLEET_ASSIGNMENT_REASONS).optional(),
238
+ selectedVersion: FleetText.optional(),
239
+ installedVersion: FleetText.optional(),
240
+ attempts: z.number().int().min(0).max(1_000_000),
241
+ attemptedAt: FleetUtcTime,
242
+ lastSuccessAt: FleetUtcTime.optional(),
243
+ })
244
+ .strict()
245
+ .superRefine((value, ctx) => {
246
+ if (value.state === "blocked" && value.reason === undefined)
247
+ ctx.addIssue({code: "custom", path: ["reason"], message: "a blocked assignment carries its reason"})
248
+ if (value.state !== "blocked" && value.state !== "retrying" && value.reason !== undefined)
249
+ ctx.addIssue({code: "custom", path: ["reason"], message: "only a blocked or retrying assignment carries a reason"})
250
+ })
251
+
157
252
  export const FleetCoverageGapPayloadSchema = z
158
253
  .object({
159
254
  reason: z.enum(["queue_dropped", "queue_expired", "clock_untrusted", "reporter_stopped", "record_rejected"]),
@@ -170,10 +265,6 @@ const base = {
170
265
  collectorSessionId: FleetId,
171
266
  sequence: z.number().int().min(0),
172
267
  observedAt: FleetUtcTime,
173
- management: z
174
- .object({associationId: FleetId, generation: z.number().int().min(0)})
175
- .strict()
176
- .optional(),
177
268
  capabilities: z.array(z.enum(FLEET_CAPABILITIES)).max(FLEET_CAPABILITIES.length),
178
269
  }
179
270
  export const FleetRecordSchema = z.discriminatedUnion("kind", [
@@ -197,6 +288,8 @@ export const FleetRecordSchema = z.discriminatedUnion("kind", [
197
288
  })
198
289
  .strict(),
199
290
  z.object({...base, kind: z.literal("coverage_gap"), payload: FleetCoverageGapPayloadSchema}).strict(),
291
+ z.object({...base, kind: z.literal("miniapp_inventory"), payload: FleetMiniappInventoryPayloadSchema}).strict(),
292
+ z.object({...base, kind: z.literal("assignment_outcome"), payload: FleetAssignmentOutcomePayloadSchema}).strict(),
200
293
  ]).superRefine((value, ctx) => {
201
294
  // A snapshot's glasses belong to the pair its binding names, so the two come together.
202
295
  if (value.kind === "snapshot" && (value.payload.glasses !== undefined) !== (value.deviceBinding !== undefined))
@@ -243,11 +336,57 @@ export const FleetReportAckSchema = z
243
336
  .object({
244
337
  schemaVersion: z.literal(FLEET_SCHEMA_VERSION),
245
338
  results: z.array(FleetRecordResultSchema),
339
+ /**
340
+ * The account's current desired-state revision (Phase 11). A phone holding an older one fetches desired state at
341
+ * once; there is no push channel. Optional so an acknowledgment from an older MFM still parses.
342
+ */
343
+ revision: FleetRevision.optional(),
246
344
  })
247
345
  .strict()
248
346
  export type FleetRecordResult = z.infer<typeof FleetRecordResultSchema>
249
347
  export type FleetReportAck = z.infer<typeof FleetReportAckSchema>
250
348
 
349
+ /**
350
+ * Desired state, as MFM answers a phone through Core (`GET /api/client/fleet/desired-state`). Every answer carries one
351
+ * monotonic per-account `revision` and its generation time.
352
+ *
353
+ * - `managed`: the account's workspace and the assignment entries, which may be empty. A member of a workspace with
354
+ * nothing assigned is still managed.
355
+ * - `unmanaged`: the account belongs to no workspace. Authoritative: the phone clears its assignment reasons.
356
+ *
357
+ * An error, timeout, partial response or missing MFM is never an empty set: MFM answers {@link FLEET_UNAVAILABLE}
358
+ * (503) and the phone keeps its last complete snapshot. `paused` (Phase 15) is not part of this schema, so a phone
359
+ * that receives an unknown status keeps its last snapshot.
360
+ */
361
+ export const FleetDesiredStateEntrySchema = z
362
+ .object({
363
+ assignmentId: FleetId,
364
+ revision: FleetRevision,
365
+ /** The miniapp's own URL (Mentra-Specs #140). The phone installs it with `installMiniapp(url)` and takes the version it serves. */
366
+ url: FleetMiniappUrl,
367
+ /** The package the URL's manifest named when it was assigned. Display and matching only; the URL decides what installs. */
368
+ packageName: FleetText,
369
+ selection: z.literal("latest"),
370
+ })
371
+ .strict()
372
+
373
+ export const FleetDesiredStateSchema = z.discriminatedUnion("status", [
374
+ z
375
+ .object({
376
+ status: z.literal("managed"),
377
+ revision: FleetRevision,
378
+ generatedAt: FleetUtcTime,
379
+ workspace: z.object({workspaceId: FleetId, name: z.string().min(1).max(200)}).strict(),
380
+ entries: z.array(FleetDesiredStateEntrySchema).max(FLEET_LIMITS.maxAssignmentEntries),
381
+ })
382
+ .strict(),
383
+ z.object({status: z.literal("unmanaged"), revision: FleetRevision, generatedAt: FleetUtcTime}).strict(),
384
+ ])
385
+ export type FleetDesiredStateEntry = z.infer<typeof FleetDesiredStateEntrySchema>
386
+ export type FleetDesiredState = z.infer<typeof FleetDesiredStateSchema>
387
+ export type FleetMiniappInventoryPayload = z.infer<typeof FleetMiniappInventoryPayloadSchema>
388
+ export type FleetAssignmentOutcomePayload = z.infer<typeof FleetAssignmentOutcomePayloadSchema>
389
+
251
390
  /** Whole-response error codes Core and MFM answer with. */
252
391
  export const FLEET_NOT_INSTALLED = "fleet_not_installed"
253
392
  export const FLEET_UNAVAILABLE = "fleet_unavailable"