@mentra/cloud-protocol 3.4.0-dev.1089 → 3.4.0-dev.1096
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/index.ts +1 -0
- package/src/miniapp-registry.ts +149 -0
- package/src/miniapp-url.ts +3 -3
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -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
|
+
})
|
package/src/miniapp-url.ts
CHANGED
|
@@ -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
|
|
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(
|
|
290
|
+
version: z.string().regex(MINIAPP_VERSION_PATTERN),
|
|
291
291
|
name: NonBlankString,
|
|
292
292
|
description: z.string().optional(),
|
|
293
293
|
icon: NonBlankString.optional(),
|