@mentra/cloud-protocol 3.4.0-dev.1071 → 3.4.0-dev.1075
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 +1 -1
- package/src/fleet.ts +143 -4
package/package.json
CHANGED
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"
|