@mentra/cloud-protocol 3.4.0-dev.1088 → 3.4.0-dev.1090

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-protocol",
3
- "version": "3.4.0-dev.1088",
3
+ "version": "3.4.0-dev.1090",
4
4
  "type": "module",
5
5
  "main": "./src/index.ts",
6
6
  "exports": {
package/src/index.ts CHANGED
@@ -19,3 +19,4 @@ export * from "./maps";
19
19
  export * from "./report-automation";
20
20
  export * from "./fleet";
21
21
  export * from "./miniapp-url";
22
+ export * from "./miniapp-registry";
@@ -0,0 +1,149 @@
1
+ /**
2
+ * @fileoverview The Mentra App's registry calls to Core: `/api/client/registry/register` and `/status`.
3
+ *
4
+ * Every install of a miniapp URL registers it. Core verifies the URL itself, with its own fetcher, and answers:
5
+ * - `accepted`, with the `miniapp.json` Core read;
6
+ * - `accepted_unverified`, when Core could not read it: the URL is reachable only from the phone, or its server asks
7
+ * for sign-in and refused the person's token. The phone's own request decides whether it installs;
8
+ * - `not_a_miniapp`, which stops the install;
9
+ * - `archived`, which stops it too.
10
+ *
11
+ * The updater sends one status batch for every installed URL and keeps the answer for `validForSeconds`. An archived
12
+ * URL is never updated; installed copies keep running.
13
+ *
14
+ * Core validates requests with these schemas, and the Cloud Client validates Core's answers with them. Objects keep
15
+ * only the fields they name, so a later Core can add fields without breaking an older phone.
16
+ */
17
+ import {z} from "zod"
18
+ import {
19
+ MINIAPP_AGE_RATINGS,
20
+ MINIAPP_PACKAGE_NAME_PATTERN,
21
+ MINIAPP_VERSION_PATTERN,
22
+ ServedMiniappManifestSchema,
23
+ type MiniappAgeRating,
24
+ type ServedMiniappManifest,
25
+ } from "./miniapp-url"
26
+
27
+ /** The most URLs one status call may name. */
28
+ export const MINIAPP_REGISTRY_STATUS_MAX_ENTRIES = 500
29
+ /** How long the phone keeps a status answer when Core doesn't say: 6 hours. */
30
+ export const MINIAPP_REGISTRY_STATUS_DEFAULT_TTL_SECONDS = 21_600
31
+
32
+ const MAX_URL_LENGTH = 2048
33
+ const MAX_NAME_LENGTH = 500
34
+ const MAX_VERSION_LENGTH = 256
35
+
36
+ /** `verified`: Core read the URL's `miniapp.json` itself. `phone_reported`: Core could not, so it holds the phone's report. */
37
+ export type MiniappRegistryVerification = "verified" | "phone_reported"
38
+ /** An entry's own status. An archive rule can also cover an `active` entry; the status call answers for both. */
39
+ export type MiniappRegistryEntryStatus = "active" | "archived"
40
+
41
+ /** One registry entry, as Core shows it to a caller. */
42
+ export interface RegistryEntryView {
43
+ /** The normalized miniapp URL: the entry's key. */
44
+ url: string
45
+ packageName: string
46
+ registrableDomain: string
47
+ /**
48
+ * Null when Core knows only the package name: a private miniapp whose server refused Core's token, or a private
49
+ * entry whose server hasn't admitted this caller.
50
+ */
51
+ name: string | null
52
+ version: string | null
53
+ ageRating: MiniappAgeRating | null
54
+ iconUrl: string | null
55
+ verification: MiniappRegistryVerification
56
+ /** Its server asks for sign-in. */
57
+ private: boolean
58
+ status: MiniappRegistryEntryStatus
59
+ /** ISO 8601. */
60
+ firstRegisteredAt: string
61
+ /** ISO 8601, or null when Core has never read it. */
62
+ lastVerifiedAt: string | null
63
+ }
64
+
65
+ /** Why a URL isn't a miniapp. */
66
+ export const MINIAPP_REGISTRY_NOT_A_MINIAPP_REASONS = [
67
+ "invalid_manifest",
68
+ "domain_mismatch",
69
+ "not_https",
70
+ "local_network_url",
71
+ ] as const
72
+ export type MiniappRegistryNotAMiniappReason = (typeof MINIAPP_REGISTRY_NOT_A_MINIAPP_REASONS)[number]
73
+
74
+ /** What the phone read from the URL, sent with every registration. Core keeps it only when it can't read the URL. */
75
+ export const RegistryReportedManifestSchema = z.object({
76
+ packageName: z.string().regex(MINIAPP_PACKAGE_NAME_PATTERN),
77
+ name: z
78
+ .string()
79
+ .max(MAX_NAME_LENGTH)
80
+ .refine((name) => name.trim().length > 0, "must not be blank"),
81
+ version: z.string().max(MAX_VERSION_LENGTH).regex(MINIAPP_VERSION_PATTERN),
82
+ /** Absent (or null) when the miniapp declares none. */
83
+ ageRating: z.enum(MINIAPP_AGE_RATINGS).nullish(),
84
+ })
85
+ export type RegistryReportedManifest = z.infer<typeof RegistryReportedManifestSchema>
86
+
87
+ export const RegistryRegisterRequestSchema = z.object({
88
+ url: z.string().min(1).max(MAX_URL_LENGTH),
89
+ reported: RegistryReportedManifestSchema.optional(),
90
+ })
91
+ export type RegistryRegisterRequest = z.infer<typeof RegistryRegisterRequestSchema>
92
+
93
+ const IsoTime = z.string().min(1)
94
+
95
+ export const RegistryEntryViewSchema: z.ZodType<RegistryEntryView> = z.object({
96
+ url: z.string().min(1),
97
+ packageName: z.string().min(1),
98
+ registrableDomain: z.string().min(1),
99
+ name: z.string().nullable(),
100
+ version: z.string().nullable(),
101
+ ageRating: z.enum(MINIAPP_AGE_RATINGS).nullable(),
102
+ iconUrl: z.string().nullable(),
103
+ verification: z.enum(["verified", "phone_reported"]),
104
+ private: z.boolean(),
105
+ status: z.enum(["active", "archived"]),
106
+ firstRegisteredAt: IsoTime,
107
+ lastVerifiedAt: IsoTime.nullable(),
108
+ })
109
+
110
+ export type RegistryRegisterResponse =
111
+ | {result: "accepted"; entry: RegistryEntryView; manifest: ServedMiniappManifest}
112
+ | {result: "accepted_unverified"; entry: RegistryEntryView}
113
+ | {result: "not_a_miniapp"; reason: MiniappRegistryNotAMiniappReason}
114
+ | {result: "archived"}
115
+
116
+ export const RegistryRegisterResponseSchema: z.ZodType<RegistryRegisterResponse> = z.discriminatedUnion("result", [
117
+ z.object({result: z.literal("accepted"), entry: RegistryEntryViewSchema, manifest: ServedMiniappManifestSchema}),
118
+ z.object({result: z.literal("accepted_unverified"), entry: RegistryEntryViewSchema}),
119
+ z.object({result: z.literal("not_a_miniapp"), reason: z.enum(MINIAPP_REGISTRY_NOT_A_MINIAPP_REASONS)}),
120
+ z.object({result: z.literal("archived")}),
121
+ ])
122
+
123
+ export const RegistryStatusRequestSchema = z.object({
124
+ entries: z
125
+ .array(
126
+ z.object({
127
+ url: z.string().min(1).max(MAX_URL_LENGTH),
128
+ /** The installed version. */
129
+ version: z.string().min(1).max(MAX_VERSION_LENGTH),
130
+ }),
131
+ )
132
+ .max(MINIAPP_REGISTRY_STATUS_MAX_ENTRIES),
133
+ })
134
+ export type RegistryStatusRequest = z.infer<typeof RegistryStatusRequestSchema>
135
+
136
+ /** `unknown`: the URL isn't in the registry, or isn't a miniapp URL. */
137
+ export type MiniappRegistryUrlStatus = "active" | "archived" | "unknown"
138
+
139
+ export interface RegistryStatusResponse {
140
+ /** One per requested entry, in order, with the URL as the phone sent it. */
141
+ statuses: Array<{url: string; status: MiniappRegistryUrlStatus}>
142
+ /** How long the answer stays valid, in seconds. */
143
+ validForSeconds: number
144
+ }
145
+
146
+ export const RegistryStatusResponseSchema: z.ZodType<RegistryStatusResponse> = z.object({
147
+ statuses: z.array(z.object({url: z.string(), status: z.enum(["active", "archived", "unknown"])})),
148
+ validForSeconds: z.number().int().positive(),
149
+ })
@@ -255,8 +255,8 @@ function isLocalIpv6(words: number[]): boolean {
255
255
  return isLoopback || isUniqueLocal || isLinkLocal
256
256
  }
257
257
 
258
- /** SemVer 2.0.0 (semver.org): `1.4.2`, `1.4.2-beta.1`; not `v1.4.2` or `1.4`. */
259
- const SEMVER =
258
+ /** SemVer 2.0.0 (semver.org): `1.4.2`, `1.4.2-beta.1`; not `v1.4.2` or `1.4`. The `version` of a served `miniapp.json`. */
259
+ export const MINIAPP_VERSION_PATTERN =
260
260
  /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/
261
261
 
262
262
  /** What a miniapp URL serves at `<url>/miniapp.json`: the manifest inside its current ZIP, plus `bundle`. */
@@ -287,7 +287,7 @@ const NonBlankString = z.string().refine((value) => value.trim().length > 0, "mu
287
287
  export const ServedMiniappManifestSchema: z.ZodType<ServedMiniappManifest> = z
288
288
  .object({
289
289
  packageName: z.string().regex(MINIAPP_PACKAGE_NAME_PATTERN),
290
- version: z.string().regex(SEMVER),
290
+ version: z.string().regex(MINIAPP_VERSION_PATTERN),
291
291
  name: NonBlankString,
292
292
  description: z.string().optional(),
293
293
  icon: NonBlankString.optional(),