@mcp-native/a2ui 0.7.0 → 0.8.0
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/README.md +39 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/v1/host-extensions.d.ts +107 -0
- package/dist/v1/host-extensions.d.ts.map +1 -0
- package/dist/v1/host-extensions.js +719 -0
- package/dist/v1/host-extensions.js.map +1 -0
- package/dist/v1/index.d.ts +4 -0
- package/dist/v1/index.d.ts.map +1 -1
- package/dist/v1/index.js +1 -0
- package/dist/v1/index.js.map +1 -1
- package/dist/v1/parse.d.ts +8 -3
- package/dist/v1/parse.d.ts.map +1 -1
- package/dist/v1/parse.js +11 -6
- package/dist/v1/parse.js.map +1 -1
- package/dist/v1/resolve.d.ts +2 -1
- package/dist/v1/resolve.d.ts.map +1 -1
- package/dist/v1/resolve.js +2 -2
- package/dist/v1/resolve.js.map +1 -1
- package/dist/v1/store.d.ts +6 -0
- package/dist/v1/store.d.ts.map +1 -1
- package/dist/v1/store.js +41 -2
- package/dist/v1/store.js.map +1 -1
- package/dist/v1/validate.d.ts +7 -0
- package/dist/v1/validate.d.ts.map +1 -1
- package/dist/v1/validate.js +38 -5
- package/dist/v1/validate.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,719 @@
|
|
|
1
|
+
import { JSON_MAX_STRING_LENGTH, JSON_MAX_TOTAL_STRING_CODE_UNITS, JSON_MAX_VALUES, isMcpExtensionIdentifier, negotiateMcpExtension, parseJsonObject, } from "@mcp-native/core";
|
|
2
|
+
import Ajv2020Import from "ajv/dist/2020.js";
|
|
3
|
+
import addFormatsImport from "ajv-formats";
|
|
4
|
+
import { A2uiParseError } from "../errors.js";
|
|
5
|
+
export const A2UI_V1_HOST_EXTENSION_PROFILE_ID = "io.mcp-native/a2ui-host-extensions";
|
|
6
|
+
export const A2UI_V1_HOST_EXTENSION_PROFILE_VERSION = "1";
|
|
7
|
+
export const A2UI_V1_HOST_EXTENSION_MAX_MANIFESTS = 64;
|
|
8
|
+
export const A2UI_V1_HOST_EXTENSION_MAX_EVENTS = 64;
|
|
9
|
+
export const A2UI_V1_HOST_EXTENSION_MAX_NEEDS = 64;
|
|
10
|
+
export const A2UI_V1_HOST_EXTENSION_MAX_INSTANCES = 256;
|
|
11
|
+
export const A2UI_V1_HOST_EXTENSION_MAX_UPDATES = 4_096;
|
|
12
|
+
const Ajv2020 = Ajv2020Import;
|
|
13
|
+
const addFormats = addFormatsImport;
|
|
14
|
+
const registryStates = new WeakMap();
|
|
15
|
+
const negotiatedResults = new WeakSet();
|
|
16
|
+
const parsedManifests = new WeakSet();
|
|
17
|
+
const compiledManifestCache = new WeakMap();
|
|
18
|
+
const RESERVED_PROP_NAMES = new Set([
|
|
19
|
+
"accessibility",
|
|
20
|
+
"action",
|
|
21
|
+
"catalogId",
|
|
22
|
+
"child",
|
|
23
|
+
"children",
|
|
24
|
+
"component",
|
|
25
|
+
"id",
|
|
26
|
+
"metadata",
|
|
27
|
+
"tabs",
|
|
28
|
+
"weight",
|
|
29
|
+
]);
|
|
30
|
+
const FORBIDDEN_EXTENSION_COMPONENT_FIELDS = new Set(["action", "child", "children", "tabs"]);
|
|
31
|
+
const STABLE_VERSION_PATTERN = /^(?:0|[1-9][0-9]*)(?:\.(?:0|[1-9][0-9]*)){0,2}$/;
|
|
32
|
+
const NEED_PATTERN = /^[a-z][a-z0-9]*(?:[._-][a-z0-9]+)*$/;
|
|
33
|
+
/** Validates and freezes one locally authored host-extension compatibility manifest. */
|
|
34
|
+
export function parseA2uiV1HostExtensionManifest(input, path = "host extension manifest") {
|
|
35
|
+
const value = parseJsonObject(input, path);
|
|
36
|
+
rejectKeys(value, [
|
|
37
|
+
"accessibility",
|
|
38
|
+
"catalogId",
|
|
39
|
+
"catalogVersion",
|
|
40
|
+
"compatibility",
|
|
41
|
+
"componentName",
|
|
42
|
+
"events",
|
|
43
|
+
"extensionId",
|
|
44
|
+
"fallback",
|
|
45
|
+
"limits",
|
|
46
|
+
"permissionNeeds",
|
|
47
|
+
"platforms",
|
|
48
|
+
"profileVersion",
|
|
49
|
+
"propsSchema",
|
|
50
|
+
"resourceNeeds",
|
|
51
|
+
"schemaVersion",
|
|
52
|
+
], path);
|
|
53
|
+
if (value.profileVersion !== A2UI_V1_HOST_EXTENSION_PROFILE_VERSION) {
|
|
54
|
+
throw new A2uiParseError(`Expected host-extension profile version ${A2UI_V1_HOST_EXTENSION_PROFILE_VERSION} at ${path}.profileVersion`);
|
|
55
|
+
}
|
|
56
|
+
const extensionId = expectString(value.extensionId, `${path}.extensionId`);
|
|
57
|
+
if (!isMcpExtensionIdentifier(extensionId)) {
|
|
58
|
+
throw new A2uiParseError(`Expected a namespaced extension identifier at ${path}.extensionId`);
|
|
59
|
+
}
|
|
60
|
+
const catalogVersion = expectStableVersion(value.catalogVersion, `${path}.catalogVersion`);
|
|
61
|
+
const schemaVersion = expectStableVersion(value.schemaVersion, `${path}.schemaVersion`);
|
|
62
|
+
const catalogId = expectString(value.catalogId, `${path}.catalogId`);
|
|
63
|
+
if (catalogId !== `${extensionId}@${catalogVersion}`) {
|
|
64
|
+
throw new A2uiParseError(`Expected ${path}.catalogId to equal the exact extension and catalog version`);
|
|
65
|
+
}
|
|
66
|
+
const componentName = expectString(value.componentName, `${path}.componentName`);
|
|
67
|
+
if (!componentName.startsWith(`${extensionId}:`) ||
|
|
68
|
+
!/^[A-Z][A-Za-z0-9]*$/.test(componentName.slice(extensionId.length + 1))) {
|
|
69
|
+
throw new A2uiParseError(`Expected a namespaced PascalCase component name at ${path}.componentName`);
|
|
70
|
+
}
|
|
71
|
+
const propsSchema = parseClosedSchema(value.propsSchema, `${path}.propsSchema`, true);
|
|
72
|
+
const events = parseEvents(value.events, extensionId, `${path}.events`);
|
|
73
|
+
const platforms = parsePlatforms(value.platforms, `${path}.platforms`);
|
|
74
|
+
const accessibilityValue = parseJsonObject(value.accessibility, `${path}.accessibility`);
|
|
75
|
+
rejectKeys(accessibilityValue, ["behavior", "ownership", "requiresLabel"], `${path}.accessibility`);
|
|
76
|
+
if (accessibilityValue.ownership !== "host") {
|
|
77
|
+
throw new A2uiParseError(`Expected the string "host" at ${path}.accessibility.ownership`);
|
|
78
|
+
}
|
|
79
|
+
const requiresLabel = expectBoolean(accessibilityValue.requiresLabel, `${path}.accessibility.requiresLabel`);
|
|
80
|
+
const behavior = expectNonEmptyBoundedString(accessibilityValue.behavior, `${path}.accessibility.behavior`);
|
|
81
|
+
const resourceNeeds = parseNeeds(value.resourceNeeds, `${path}.resourceNeeds`);
|
|
82
|
+
const permissionNeeds = parseNeeds(value.permissionNeeds, `${path}.permissionNeeds`);
|
|
83
|
+
const limitsValue = parseJsonObject(value.limits, `${path}.limits`);
|
|
84
|
+
rejectKeys(limitsValue, [
|
|
85
|
+
"maximumInstances",
|
|
86
|
+
"maximumEventPayloadStringCodeUnits",
|
|
87
|
+
"maximumEventPayloadValues",
|
|
88
|
+
"maximumPropsStringCodeUnits",
|
|
89
|
+
"maximumPropsValues",
|
|
90
|
+
"maximumUpdatesPerSurface",
|
|
91
|
+
], `${path}.limits`);
|
|
92
|
+
const maximumInstances = expectInteger(limitsValue.maximumInstances, 1, A2UI_V1_HOST_EXTENSION_MAX_INSTANCES, `${path}.limits.maximumInstances`);
|
|
93
|
+
const maximumEventPayloadValues = expectInteger(limitsValue.maximumEventPayloadValues, 1, JSON_MAX_VALUES, `${path}.limits.maximumEventPayloadValues`);
|
|
94
|
+
const maximumEventPayloadStringCodeUnits = expectInteger(limitsValue.maximumEventPayloadStringCodeUnits, 1, JSON_MAX_TOTAL_STRING_CODE_UNITS, `${path}.limits.maximumEventPayloadStringCodeUnits`);
|
|
95
|
+
const maximumPropsValues = expectInteger(limitsValue.maximumPropsValues, 1, JSON_MAX_VALUES, `${path}.limits.maximumPropsValues`);
|
|
96
|
+
const maximumPropsStringCodeUnits = expectInteger(limitsValue.maximumPropsStringCodeUnits, 1, JSON_MAX_TOTAL_STRING_CODE_UNITS, `${path}.limits.maximumPropsStringCodeUnits`);
|
|
97
|
+
const maximumUpdatesPerSurface = expectInteger(limitsValue.maximumUpdatesPerSurface, 1, A2UI_V1_HOST_EXTENSION_MAX_UPDATES, `${path}.limits.maximumUpdatesPerSurface`);
|
|
98
|
+
const fallbackValue = parseJsonObject(value.fallback, `${path}.fallback`);
|
|
99
|
+
rejectKeys(fallbackValue, ["kind"], `${path}.fallback`);
|
|
100
|
+
if (fallbackValue.kind !== "reject") {
|
|
101
|
+
throw new A2uiParseError(`Host-extension fallback must be the fail-closed "reject" behavior at ${path}.fallback.kind`);
|
|
102
|
+
}
|
|
103
|
+
const compatibilityValue = parseJsonObject(value.compatibility, `${path}.compatibility`);
|
|
104
|
+
rejectKeys(compatibilityValue, ["owner", "supportUrl"], `${path}.compatibility`);
|
|
105
|
+
const owner = expectNonEmptyBoundedString(compatibilityValue.owner, `${path}.compatibility.owner`);
|
|
106
|
+
const supportUrl = compatibilityValue.supportUrl === undefined
|
|
107
|
+
? undefined
|
|
108
|
+
: parseHttpsUrl(compatibilityValue.supportUrl, `${path}.compatibility.supportUrl`);
|
|
109
|
+
const manifest = Object.freeze({
|
|
110
|
+
profileVersion: A2UI_V1_HOST_EXTENSION_PROFILE_VERSION,
|
|
111
|
+
extensionId,
|
|
112
|
+
catalogId,
|
|
113
|
+
catalogVersion,
|
|
114
|
+
schemaVersion,
|
|
115
|
+
componentName,
|
|
116
|
+
propsSchema,
|
|
117
|
+
events,
|
|
118
|
+
platforms,
|
|
119
|
+
accessibility: Object.freeze({ ownership: "host", requiresLabel, behavior }),
|
|
120
|
+
resourceNeeds,
|
|
121
|
+
permissionNeeds,
|
|
122
|
+
limits: Object.freeze({
|
|
123
|
+
maximumInstances,
|
|
124
|
+
maximumEventPayloadValues,
|
|
125
|
+
maximumEventPayloadStringCodeUnits,
|
|
126
|
+
maximumPropsValues,
|
|
127
|
+
maximumPropsStringCodeUnits,
|
|
128
|
+
maximumUpdatesPerSurface,
|
|
129
|
+
}),
|
|
130
|
+
fallback: Object.freeze({ kind: "reject" }),
|
|
131
|
+
compatibility: Object.freeze({
|
|
132
|
+
owner,
|
|
133
|
+
...(supportUrl === undefined ? {} : { supportUrl }),
|
|
134
|
+
}),
|
|
135
|
+
});
|
|
136
|
+
parsedManifests.add(manifest);
|
|
137
|
+
return manifest;
|
|
138
|
+
}
|
|
139
|
+
/** Constructs the exact MCP capability map a host may advertise for local manifests. */
|
|
140
|
+
export function createA2uiV1HostExtensionCapabilitySettings(manifests, platform) {
|
|
141
|
+
const parsedPlatform = parsePlatform(platform, "host platform");
|
|
142
|
+
const parsed = parseManifestList(manifests).filter((manifest) => manifest.platforms.includes(parsedPlatform));
|
|
143
|
+
const settings = freezeCapabilitySettings(parsed.map(toCapabilityEntry));
|
|
144
|
+
return Object.freeze({
|
|
145
|
+
[A2UI_V1_HOST_EXTENSION_PROFILE_ID]: settings,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** Parses the project-owned settings value carried under the profile extension ID. */
|
|
149
|
+
export function parseA2uiV1HostExtensionCapabilityValue(input, path = "host extension capabilities") {
|
|
150
|
+
const value = parseJsonObject(input, path);
|
|
151
|
+
rejectKeys(value, ["extensions", "profileVersion"], path);
|
|
152
|
+
if (value.profileVersion !== A2UI_V1_HOST_EXTENSION_PROFILE_VERSION) {
|
|
153
|
+
throw new A2uiParseError(`Expected host-extension profile version ${A2UI_V1_HOST_EXTENSION_PROFILE_VERSION} at ${path}.profileVersion`);
|
|
154
|
+
}
|
|
155
|
+
if (!Array.isArray(value.extensions) ||
|
|
156
|
+
value.extensions.length > A2UI_V1_HOST_EXTENSION_MAX_MANIFESTS) {
|
|
157
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_HOST_EXTENSION_MAX_MANIFESTS} entries at ${path}.extensions`);
|
|
158
|
+
}
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
const extensions = value.extensions.map((entry, index) => {
|
|
161
|
+
const entryPath = `${path}.extensions[${index}]`;
|
|
162
|
+
const object = parseJsonObject(entry, entryPath);
|
|
163
|
+
rejectKeys(object, ["catalogId", "catalogVersion", "componentName", "extensionId", "schemaVersion"], entryPath);
|
|
164
|
+
const extensionId = expectString(object.extensionId, `${entryPath}.extensionId`);
|
|
165
|
+
if (!isMcpExtensionIdentifier(extensionId)) {
|
|
166
|
+
throw new A2uiParseError(`Expected a namespaced identifier at ${entryPath}.extensionId`);
|
|
167
|
+
}
|
|
168
|
+
const catalogVersion = expectStableVersion(object.catalogVersion, `${entryPath}.catalogVersion`);
|
|
169
|
+
const schemaVersion = expectStableVersion(object.schemaVersion, `${entryPath}.schemaVersion`);
|
|
170
|
+
const catalogId = expectString(object.catalogId, `${entryPath}.catalogId`);
|
|
171
|
+
const componentName = expectString(object.componentName, `${entryPath}.componentName`);
|
|
172
|
+
if (catalogId !== `${extensionId}@${catalogVersion}` ||
|
|
173
|
+
!componentName.startsWith(`${extensionId}:`) ||
|
|
174
|
+
!/^[A-Z][A-Za-z0-9]*$/.test(componentName.slice(extensionId.length + 1))) {
|
|
175
|
+
throw new A2uiParseError(`Inconsistent namespaced identity at ${entryPath}`);
|
|
176
|
+
}
|
|
177
|
+
const parsed = Object.freeze({
|
|
178
|
+
extensionId,
|
|
179
|
+
catalogId,
|
|
180
|
+
catalogVersion,
|
|
181
|
+
schemaVersion,
|
|
182
|
+
componentName,
|
|
183
|
+
});
|
|
184
|
+
const key = capabilityKey(parsed);
|
|
185
|
+
if (seen.has(key)) {
|
|
186
|
+
throw new A2uiParseError(`Duplicate host-extension capability at ${entryPath}`);
|
|
187
|
+
}
|
|
188
|
+
seen.add(key);
|
|
189
|
+
return parsed;
|
|
190
|
+
});
|
|
191
|
+
return freezeCapabilitySettings(extensions);
|
|
192
|
+
}
|
|
193
|
+
/** Negotiates only byte-for-byte identity/version tuples advertised by both MCP peers. */
|
|
194
|
+
export function negotiateA2uiV1HostExtensions(hostExtensions, serverExtensions) {
|
|
195
|
+
const generic = negotiateMcpExtension(A2UI_V1_HOST_EXTENSION_PROFILE_ID, hostExtensions, serverExtensions);
|
|
196
|
+
if (generic.kind === "fallback") {
|
|
197
|
+
return Object.freeze({
|
|
198
|
+
kind: "fallback",
|
|
199
|
+
profileId: A2UI_V1_HOST_EXTENSION_PROFILE_ID,
|
|
200
|
+
reason: generic.reason === "client-unsupported" ? "host-unsupported" : "server-unsupported",
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const host = parseA2uiV1HostExtensionCapabilityValue(generic.clientSettings, "host extension settings");
|
|
204
|
+
const server = parseA2uiV1HostExtensionCapabilityValue(generic.serverSettings, "server extension settings");
|
|
205
|
+
const serverKeys = new Set(server.extensions.map(capabilityKey));
|
|
206
|
+
const extensions = host.extensions.filter((entry) => serverKeys.has(capabilityKey(entry)));
|
|
207
|
+
if (extensions.length === 0) {
|
|
208
|
+
return Object.freeze({
|
|
209
|
+
kind: "fallback",
|
|
210
|
+
profileId: A2UI_V1_HOST_EXTENSION_PROFILE_ID,
|
|
211
|
+
reason: "no-exact-extension-match",
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
const result = Object.freeze({
|
|
215
|
+
kind: "negotiated",
|
|
216
|
+
profileId: A2UI_V1_HOST_EXTENSION_PROFILE_ID,
|
|
217
|
+
profileVersion: A2UI_V1_HOST_EXTENSION_PROFILE_VERSION,
|
|
218
|
+
extensions: Object.freeze(extensions),
|
|
219
|
+
inlineCatalogsEnabled: false,
|
|
220
|
+
});
|
|
221
|
+
negotiatedResults.add(result);
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
/** Creates an opaque registry containing only local, platform-available, exactly negotiated entries. */
|
|
225
|
+
export function createA2uiV1HostExtensionRegistry(options) {
|
|
226
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
227
|
+
throw new A2uiParseError("Expected host-extension registry options to be an object");
|
|
228
|
+
}
|
|
229
|
+
const platform = parsePlatform(options.platform, "registry platform");
|
|
230
|
+
if (options.negotiation?.kind !== "negotiated" || !negotiatedResults.has(options.negotiation)) {
|
|
231
|
+
throw new A2uiParseError("Host-extension registry requires an exact negotiated capability set");
|
|
232
|
+
}
|
|
233
|
+
const negotiatedKeys = new Set(options.negotiation.extensions.map(capabilityKey));
|
|
234
|
+
const manifests = parseManifestList(options.manifests).filter((manifest) => manifest.platforms.includes(platform) && negotiatedKeys.has(capabilityKey(manifest)));
|
|
235
|
+
if (manifests.length !== options.negotiation.extensions.length) {
|
|
236
|
+
throw new A2uiParseError("Every negotiated host extension must have one exact locally available manifest");
|
|
237
|
+
}
|
|
238
|
+
const byComponentKey = new Map();
|
|
239
|
+
for (const manifest of manifests) {
|
|
240
|
+
const key = componentKey(manifest.catalogId, manifest.componentName);
|
|
241
|
+
if (byComponentKey.has(key)) {
|
|
242
|
+
throw new A2uiParseError(`Duplicate local host-extension component ${manifest.componentName}`);
|
|
243
|
+
}
|
|
244
|
+
byComponentKey.set(key, compileManifest(manifest));
|
|
245
|
+
}
|
|
246
|
+
const registry = Object.freeze({ platform, manifests: Object.freeze(manifests) });
|
|
247
|
+
registryStates.set(registry, { byComponentKey });
|
|
248
|
+
return registry;
|
|
249
|
+
}
|
|
250
|
+
export function isA2uiV1HostExtensionRegistry(value) {
|
|
251
|
+
return (value !== null &&
|
|
252
|
+
typeof value === "object" &&
|
|
253
|
+
registryStates.has(value));
|
|
254
|
+
}
|
|
255
|
+
/** Returns the exact local manifest for a component, or undefined for a basic/unknown component. */
|
|
256
|
+
export function getA2uiV1HostExtensionManifest(registry, catalogId, componentName) {
|
|
257
|
+
const state = requireRegistry(registry);
|
|
258
|
+
if (typeof catalogId !== "string" || typeof componentName !== "string") {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
return state.byComponentKey.get(componentKey(catalogId, componentName))?.manifest;
|
|
262
|
+
}
|
|
263
|
+
/** Exact negotiated catalog IDs available on this registry's selected native platform. */
|
|
264
|
+
export function getA2uiV1HostExtensionCatalogIds(registry) {
|
|
265
|
+
requireRegistry(registry);
|
|
266
|
+
return Object.freeze([...new Set(registry.manifests.map((manifest) => manifest.catalogId))]);
|
|
267
|
+
}
|
|
268
|
+
/** Validates one leaf extension component and reconstructs only its declared semantic props. */
|
|
269
|
+
export function validateA2uiV1HostExtensionComponent(registry, input, path = "host extension component") {
|
|
270
|
+
const component = parseJsonObject(input, path);
|
|
271
|
+
const catalogId = expectString(component.catalogId, `${path}.catalogId`);
|
|
272
|
+
const componentName = expectString(component.component, `${path}.component`);
|
|
273
|
+
const compiled = requireRegistry(registry).byComponentKey.get(componentKey(catalogId, componentName));
|
|
274
|
+
if (compiled === undefined) {
|
|
275
|
+
throw new A2uiParseError(`Unknown, unavailable, or unnegotiated host extension ${JSON.stringify(componentName)} at ${path}`);
|
|
276
|
+
}
|
|
277
|
+
const props = {};
|
|
278
|
+
for (const [name, value] of Object.entries(component)) {
|
|
279
|
+
if (FORBIDDEN_EXTENSION_COMPONENT_FIELDS.has(name)) {
|
|
280
|
+
throw new A2uiParseError(`Host-extension components are leaves and cannot declare ${JSON.stringify(name)} at ${path}`);
|
|
281
|
+
}
|
|
282
|
+
if (!RESERVED_PROP_NAMES.has(name)) {
|
|
283
|
+
Object.defineProperty(props, name, {
|
|
284
|
+
value,
|
|
285
|
+
enumerable: true,
|
|
286
|
+
configurable: true,
|
|
287
|
+
writable: true,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const parsedProps = parseJsonObject(props, `${path}.props`, {
|
|
292
|
+
maxTotalStringCodeUnits: compiled.manifest.limits.maximumPropsStringCodeUnits,
|
|
293
|
+
});
|
|
294
|
+
const budget = measureJson(parsedProps);
|
|
295
|
+
if (budget.values > compiled.manifest.limits.maximumPropsValues) {
|
|
296
|
+
throw new A2uiParseError(`Host-extension props exceed maximum of ${compiled.manifest.limits.maximumPropsValues} values at ${path}`);
|
|
297
|
+
}
|
|
298
|
+
if (!compiled.propsValidator(parsedProps)) {
|
|
299
|
+
throw new A2uiParseError(`Host-extension props schema validation failed at ${path}: ${formatAjvErrors(compiled.propsValidator)}`);
|
|
300
|
+
}
|
|
301
|
+
const frozenProps = deepFreezeJson(parsedProps);
|
|
302
|
+
return Object.freeze({
|
|
303
|
+
manifest: compiled.manifest,
|
|
304
|
+
props: frozenProps,
|
|
305
|
+
manifestFingerprint: compiled.fingerprint,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
/** Revalidates a locally emitted extension event before it reaches a host transport callback. */
|
|
309
|
+
export function validateA2uiV1HostExtensionEvent(manifest, eventName, payload, userActivated) {
|
|
310
|
+
const parsedManifest = parsedManifests.has(manifest)
|
|
311
|
+
? manifest
|
|
312
|
+
: parseA2uiV1HostExtensionManifest(manifest);
|
|
313
|
+
const compiled = compileManifest(parsedManifest);
|
|
314
|
+
if (typeof eventName !== "string") {
|
|
315
|
+
throw new A2uiParseError("Expected a string host-extension event name");
|
|
316
|
+
}
|
|
317
|
+
const event = compiled.eventManifests.get(eventName);
|
|
318
|
+
const validator = compiled.eventValidators.get(eventName);
|
|
319
|
+
if (event === undefined || validator === undefined) {
|
|
320
|
+
throw new A2uiParseError(`Unknown host-extension event ${JSON.stringify(eventName)}`);
|
|
321
|
+
}
|
|
322
|
+
if (event.requiresUserActivation && userActivated !== true) {
|
|
323
|
+
throw new A2uiParseError(`Host-extension event ${JSON.stringify(eventName)} requires explicit user activation`);
|
|
324
|
+
}
|
|
325
|
+
if (userActivated !== true && userActivated !== false) {
|
|
326
|
+
throw new A2uiParseError("Expected a boolean host-extension user-activation marker");
|
|
327
|
+
}
|
|
328
|
+
const parsedPayload = parseJsonObject(payload, `host extension event ${eventName}`, {
|
|
329
|
+
maxTotalStringCodeUnits: parsedManifest.limits.maximumEventPayloadStringCodeUnits,
|
|
330
|
+
});
|
|
331
|
+
if (measureJson(parsedPayload).values > parsedManifest.limits.maximumEventPayloadValues) {
|
|
332
|
+
throw new A2uiParseError(`Host-extension event exceeds maximum of ${parsedManifest.limits.maximumEventPayloadValues} values`);
|
|
333
|
+
}
|
|
334
|
+
if (!validator(parsedPayload)) {
|
|
335
|
+
throw new A2uiParseError(`Host-extension event schema validation failed: ${formatAjvErrors(validator)}`);
|
|
336
|
+
}
|
|
337
|
+
return parsedPayload;
|
|
338
|
+
}
|
|
339
|
+
export function getA2uiV1HostExtensionManifestFingerprint(manifest) {
|
|
340
|
+
const parsed = parsedManifests.has(manifest)
|
|
341
|
+
? manifest
|
|
342
|
+
: parseA2uiV1HostExtensionManifest(manifest);
|
|
343
|
+
return canonicalizeJson(parsed);
|
|
344
|
+
}
|
|
345
|
+
/** Internal parser seam: validates extension leaves while preserving the pinned envelope schema. */
|
|
346
|
+
export function validateA2uiV1EnvelopeHostExtensions(envelope, registry, validateBaseEnvelope) {
|
|
347
|
+
requireRegistry(registry);
|
|
348
|
+
const messageName = Object.hasOwn(envelope, "createSurface")
|
|
349
|
+
? "createSurface"
|
|
350
|
+
: Object.hasOwn(envelope, "updateComponents")
|
|
351
|
+
? "updateComponents"
|
|
352
|
+
: undefined;
|
|
353
|
+
if (messageName === undefined) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
const message = parseJsonObject(envelope[messageName], `envelope.${messageName}`);
|
|
357
|
+
if (!Array.isArray(message.components)) {
|
|
358
|
+
return false;
|
|
359
|
+
}
|
|
360
|
+
let extensionCount = 0;
|
|
361
|
+
const components = message.components.map((input, index) => {
|
|
362
|
+
const path = `envelope.${messageName}.components[${index}]`;
|
|
363
|
+
const component = parseJsonObject(input, path);
|
|
364
|
+
const manifest = getA2uiV1HostExtensionManifest(registry, component.catalogId, component.component);
|
|
365
|
+
if (manifest === undefined) {
|
|
366
|
+
return component;
|
|
367
|
+
}
|
|
368
|
+
validateA2uiV1HostExtensionComponent(registry, component, path);
|
|
369
|
+
extensionCount += 1;
|
|
370
|
+
return {
|
|
371
|
+
id: component.id,
|
|
372
|
+
component: "Text",
|
|
373
|
+
text: "",
|
|
374
|
+
...(component.accessibility === undefined ? {} : { accessibility: component.accessibility }),
|
|
375
|
+
...(component.metadata === undefined ? {} : { metadata: component.metadata }),
|
|
376
|
+
...(component.weight === undefined ? {} : { weight: component.weight }),
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
if (extensionCount === 0) {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
const substituted = {
|
|
383
|
+
...envelope,
|
|
384
|
+
[messageName]: { ...message, components },
|
|
385
|
+
};
|
|
386
|
+
if (!validateBaseEnvelope(substituted)) {
|
|
387
|
+
throw new A2uiParseError(`A2UI v1 host-extension envelope validation failed: ${formatAjvErrors(validateBaseEnvelope)}`);
|
|
388
|
+
}
|
|
389
|
+
return true;
|
|
390
|
+
}
|
|
391
|
+
function parseManifestList(value) {
|
|
392
|
+
if (!Array.isArray(value) || value.length > A2UI_V1_HOST_EXTENSION_MAX_MANIFESTS) {
|
|
393
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_HOST_EXTENSION_MAX_MANIFESTS} host-extension manifests`);
|
|
394
|
+
}
|
|
395
|
+
const manifests = value.map((manifest, index) => parseA2uiV1HostExtensionManifest(manifest, `host extension manifests[${index}]`));
|
|
396
|
+
const keys = new Set();
|
|
397
|
+
for (const manifest of manifests) {
|
|
398
|
+
const key = capabilityKey(manifest);
|
|
399
|
+
if (keys.has(key)) {
|
|
400
|
+
throw new A2uiParseError(`Duplicate host-extension manifest ${manifest.componentName}`);
|
|
401
|
+
}
|
|
402
|
+
keys.add(key);
|
|
403
|
+
}
|
|
404
|
+
return manifests;
|
|
405
|
+
}
|
|
406
|
+
function parseEvents(value, extensionId, path) {
|
|
407
|
+
if (!Array.isArray(value) || value.length > A2UI_V1_HOST_EXTENSION_MAX_EVENTS) {
|
|
408
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_HOST_EXTENSION_MAX_EVENTS} entries at ${path}`);
|
|
409
|
+
}
|
|
410
|
+
const names = new Set();
|
|
411
|
+
return Object.freeze(value.map((entry, index) => {
|
|
412
|
+
const entryPath = `${path}[${index}]`;
|
|
413
|
+
const object = parseJsonObject(entry, entryPath);
|
|
414
|
+
rejectKeys(object, ["name", "payloadSchema", "requiresUserActivation"], entryPath);
|
|
415
|
+
const name = expectString(object.name, `${entryPath}.name`);
|
|
416
|
+
if (!name.startsWith(`${extensionId}:`) ||
|
|
417
|
+
!/^[a-z][A-Za-z0-9]*$/.test(name.slice(extensionId.length + 1))) {
|
|
418
|
+
throw new A2uiParseError(`Expected a namespaced event name at ${entryPath}.name`);
|
|
419
|
+
}
|
|
420
|
+
if (names.has(name)) {
|
|
421
|
+
throw new A2uiParseError(`Duplicate host-extension event ${JSON.stringify(name)}`);
|
|
422
|
+
}
|
|
423
|
+
names.add(name);
|
|
424
|
+
return Object.freeze({
|
|
425
|
+
name,
|
|
426
|
+
payloadSchema: parseClosedSchema(object.payloadSchema, `${entryPath}.payloadSchema`, false),
|
|
427
|
+
requiresUserActivation: expectBoolean(object.requiresUserActivation, `${entryPath}.requiresUserActivation`),
|
|
428
|
+
});
|
|
429
|
+
}));
|
|
430
|
+
}
|
|
431
|
+
function parseClosedSchema(value, path, rejectReservedProps) {
|
|
432
|
+
const schema = parseJsonObject(value, path);
|
|
433
|
+
if (schema.type !== "object") {
|
|
434
|
+
throw new A2uiParseError(`Expected an object JSON Schema at ${path}`);
|
|
435
|
+
}
|
|
436
|
+
const properties = parseJsonObject(schema.properties, `${path}.properties`);
|
|
437
|
+
if (rejectReservedProps) {
|
|
438
|
+
for (const name of Object.keys(properties)) {
|
|
439
|
+
if (RESERVED_PROP_NAMES.has(name)) {
|
|
440
|
+
throw new A2uiParseError(`Reserved host-extension prop ${JSON.stringify(name)} at ${path}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
assertClosedObjectSchemas(schema, path);
|
|
445
|
+
rejectRemoteReferences(schema, path);
|
|
446
|
+
compileSchema(schema, path);
|
|
447
|
+
return deepFreezeJson(schema);
|
|
448
|
+
}
|
|
449
|
+
function assertClosedObjectSchemas(value, path) {
|
|
450
|
+
const pending = [{ value, path }];
|
|
451
|
+
while (pending.length > 0) {
|
|
452
|
+
const current = pending.pop();
|
|
453
|
+
if (Array.isArray(current.value)) {
|
|
454
|
+
current.value.forEach((child, index) => pending.push({ value: child, path: `${current.path}[${index}]` }));
|
|
455
|
+
continue;
|
|
456
|
+
}
|
|
457
|
+
if (current.value === null || typeof current.value !== "object") {
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
const schema = current.value;
|
|
461
|
+
const typeIncludesObject = schema.type === "object" || (Array.isArray(schema.type) && schema.type.includes("object"));
|
|
462
|
+
const declaresObjectShape = typeIncludesObject ||
|
|
463
|
+
Object.hasOwn(schema, "properties") ||
|
|
464
|
+
Object.hasOwn(schema, "patternProperties") ||
|
|
465
|
+
Object.hasOwn(schema, "required");
|
|
466
|
+
if (declaresObjectShape) {
|
|
467
|
+
const hasAdditionalProperties = Object.hasOwn(schema, "additionalProperties");
|
|
468
|
+
if ((hasAdditionalProperties && schema.additionalProperties !== false) ||
|
|
469
|
+
(!hasAdditionalProperties && schema.unevaluatedProperties !== false)) {
|
|
470
|
+
throw new A2uiParseError(`Expected a closed JSON Schema at ${current.path}`);
|
|
471
|
+
}
|
|
472
|
+
if (Object.hasOwn(schema, "patternProperties") &&
|
|
473
|
+
Object.keys(parseJsonObject(schema.patternProperties, `${current.path}.patternProperties`))
|
|
474
|
+
.length > 0) {
|
|
475
|
+
throw new A2uiParseError(`Pattern-based host-extension properties are not allowed at ${current.path}.patternProperties`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const [name, child] of Object.entries(schema)) {
|
|
479
|
+
pending.push({ value: child, path: `${current.path}.${name}` });
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
function rejectRemoteReferences(value, path) {
|
|
484
|
+
const pending = [{ value, path }];
|
|
485
|
+
while (pending.length > 0) {
|
|
486
|
+
const current = pending.pop();
|
|
487
|
+
if (Array.isArray(current.value)) {
|
|
488
|
+
current.value.forEach((child, index) => pending.push({ value: child, path: `${current.path}[${index}]` }));
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
if (current.value === null || typeof current.value !== "object") {
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
for (const [name, child] of Object.entries(current.value)) {
|
|
495
|
+
if ((name === "$ref" || name === "$dynamicRef") &&
|
|
496
|
+
typeof child === "string" &&
|
|
497
|
+
!child.startsWith("#")) {
|
|
498
|
+
throw new A2uiParseError(`Remote JSON Schema references are not allowed at ${current.path}.${name}`);
|
|
499
|
+
}
|
|
500
|
+
pending.push({ value: child, path: `${current.path}.${name}` });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
function compileManifest(manifest) {
|
|
505
|
+
const cached = compiledManifestCache.get(manifest);
|
|
506
|
+
if (cached !== undefined) {
|
|
507
|
+
return cached;
|
|
508
|
+
}
|
|
509
|
+
const eventValidators = new Map();
|
|
510
|
+
const eventManifests = new Map();
|
|
511
|
+
for (const event of manifest.events) {
|
|
512
|
+
eventValidators.set(event.name, compileSchema(event.payloadSchema, `event ${event.name}`));
|
|
513
|
+
eventManifests.set(event.name, event);
|
|
514
|
+
}
|
|
515
|
+
const compiled = {
|
|
516
|
+
manifest,
|
|
517
|
+
fingerprint: canonicalizeJson(manifest),
|
|
518
|
+
propsValidator: compileSchema(manifest.propsSchema, `props for ${manifest.componentName}`),
|
|
519
|
+
eventValidators,
|
|
520
|
+
eventManifests,
|
|
521
|
+
};
|
|
522
|
+
compiledManifestCache.set(manifest, compiled);
|
|
523
|
+
return compiled;
|
|
524
|
+
}
|
|
525
|
+
function compileSchema(schema, path) {
|
|
526
|
+
try {
|
|
527
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false, validateSchema: true });
|
|
528
|
+
addFormats(ajv);
|
|
529
|
+
return ajv.compile(schema);
|
|
530
|
+
}
|
|
531
|
+
catch (cause) {
|
|
532
|
+
throw new A2uiParseError(`Invalid host-extension JSON Schema at ${path}`, { cause });
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
function parsePlatforms(value, path) {
|
|
536
|
+
if (!Array.isArray(value) || value.length === 0 || value.length > 2) {
|
|
537
|
+
throw new A2uiParseError(`Expected one or two host platforms at ${path}`);
|
|
538
|
+
}
|
|
539
|
+
const platforms = value.map((entry, index) => parsePlatform(entry, `${path}[${index}]`));
|
|
540
|
+
if (new Set(platforms).size !== platforms.length) {
|
|
541
|
+
throw new A2uiParseError(`Expected unique host platforms at ${path}`);
|
|
542
|
+
}
|
|
543
|
+
return Object.freeze(platforms);
|
|
544
|
+
}
|
|
545
|
+
function parsePlatform(value, path) {
|
|
546
|
+
if (value !== "android" && value !== "ios") {
|
|
547
|
+
throw new A2uiParseError(`Expected "android" or "ios" at ${path}`);
|
|
548
|
+
}
|
|
549
|
+
return value;
|
|
550
|
+
}
|
|
551
|
+
function parseNeeds(value, path) {
|
|
552
|
+
if (!Array.isArray(value) || value.length > A2UI_V1_HOST_EXTENSION_MAX_NEEDS) {
|
|
553
|
+
throw new A2uiParseError(`Expected at most ${A2UI_V1_HOST_EXTENSION_MAX_NEEDS} capability needs at ${path}`);
|
|
554
|
+
}
|
|
555
|
+
const needs = value.map((entry, index) => {
|
|
556
|
+
const need = expectString(entry, `${path}[${index}]`);
|
|
557
|
+
if (!NEED_PATTERN.test(need)) {
|
|
558
|
+
throw new A2uiParseError(`Expected a closed capability identifier at ${path}[${index}]`);
|
|
559
|
+
}
|
|
560
|
+
return need;
|
|
561
|
+
});
|
|
562
|
+
if (new Set(needs).size !== needs.length) {
|
|
563
|
+
throw new A2uiParseError(`Expected unique capability needs at ${path}`);
|
|
564
|
+
}
|
|
565
|
+
return Object.freeze(needs);
|
|
566
|
+
}
|
|
567
|
+
function freezeCapabilitySettings(extensions) {
|
|
568
|
+
return Object.freeze({
|
|
569
|
+
profileVersion: A2UI_V1_HOST_EXTENSION_PROFILE_VERSION,
|
|
570
|
+
extensions: Object.freeze([...extensions]),
|
|
571
|
+
});
|
|
572
|
+
}
|
|
573
|
+
function toCapabilityEntry(manifest) {
|
|
574
|
+
return Object.freeze({
|
|
575
|
+
extensionId: manifest.extensionId,
|
|
576
|
+
catalogId: manifest.catalogId,
|
|
577
|
+
catalogVersion: manifest.catalogVersion,
|
|
578
|
+
schemaVersion: manifest.schemaVersion,
|
|
579
|
+
componentName: manifest.componentName,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
function capabilityKey(entry) {
|
|
583
|
+
return [
|
|
584
|
+
entry.extensionId,
|
|
585
|
+
entry.catalogId,
|
|
586
|
+
entry.catalogVersion,
|
|
587
|
+
entry.schemaVersion,
|
|
588
|
+
entry.componentName,
|
|
589
|
+
].join("\u0000");
|
|
590
|
+
}
|
|
591
|
+
function componentKey(catalogId, componentName) {
|
|
592
|
+
return `${catalogId}\u0000${componentName}`;
|
|
593
|
+
}
|
|
594
|
+
function requireRegistry(registry) {
|
|
595
|
+
const state = registryStates.get(registry);
|
|
596
|
+
if (state === undefined) {
|
|
597
|
+
throw new A2uiParseError("Expected an opaque host-extension registry created by this package");
|
|
598
|
+
}
|
|
599
|
+
return state;
|
|
600
|
+
}
|
|
601
|
+
function parseHttpsUrl(value, path) {
|
|
602
|
+
const source = expectString(value, path);
|
|
603
|
+
const UrlConstructor = globalThis.URL;
|
|
604
|
+
if (UrlConstructor === undefined) {
|
|
605
|
+
throw new A2uiParseError(`The host runtime cannot validate an HTTPS URL at ${path}`);
|
|
606
|
+
}
|
|
607
|
+
let parsed;
|
|
608
|
+
try {
|
|
609
|
+
parsed = new UrlConstructor(source);
|
|
610
|
+
}
|
|
611
|
+
catch (cause) {
|
|
612
|
+
throw new A2uiParseError(`Expected an absolute HTTPS URL at ${path}`, { cause });
|
|
613
|
+
}
|
|
614
|
+
if (parsed.protocol !== "https:" ||
|
|
615
|
+
parsed.username.length > 0 ||
|
|
616
|
+
parsed.password.length > 0 ||
|
|
617
|
+
parsed.hash.length > 0) {
|
|
618
|
+
throw new A2uiParseError(`Expected a credential-free HTTPS URL without a fragment at ${path}`);
|
|
619
|
+
}
|
|
620
|
+
return parsed.href;
|
|
621
|
+
}
|
|
622
|
+
function expectStableVersion(value, path) {
|
|
623
|
+
const version = expectString(value, path);
|
|
624
|
+
if (!STABLE_VERSION_PATTERN.test(version)) {
|
|
625
|
+
throw new A2uiParseError(`Expected a stable numeric version at ${path}`);
|
|
626
|
+
}
|
|
627
|
+
return version;
|
|
628
|
+
}
|
|
629
|
+
function expectString(value, path) {
|
|
630
|
+
if (typeof value !== "string" || value.length === 0 || value.length > JSON_MAX_STRING_LENGTH) {
|
|
631
|
+
throw new A2uiParseError(`Expected a non-empty bounded string at ${path}`);
|
|
632
|
+
}
|
|
633
|
+
return value;
|
|
634
|
+
}
|
|
635
|
+
function expectNonEmptyBoundedString(value, path) {
|
|
636
|
+
return expectString(value, path);
|
|
637
|
+
}
|
|
638
|
+
function expectBoolean(value, path) {
|
|
639
|
+
if (typeof value !== "boolean") {
|
|
640
|
+
throw new A2uiParseError(`Expected a boolean at ${path}`);
|
|
641
|
+
}
|
|
642
|
+
return value;
|
|
643
|
+
}
|
|
644
|
+
function expectInteger(value, minimum, maximum, path) {
|
|
645
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) {
|
|
646
|
+
throw new A2uiParseError(`Expected an integer from ${minimum} through ${maximum} at ${path}`);
|
|
647
|
+
}
|
|
648
|
+
return value;
|
|
649
|
+
}
|
|
650
|
+
function rejectKeys(value, allowed, path) {
|
|
651
|
+
const names = new Set(allowed);
|
|
652
|
+
for (const key of Object.keys(value)) {
|
|
653
|
+
if (!names.has(key)) {
|
|
654
|
+
throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function formatAjvErrors(validate) {
|
|
659
|
+
return (validate.errors?.map((error) => `${error.instancePath || "/"} ${error.message}`).join("; ") ??
|
|
660
|
+
"schema validation failed");
|
|
661
|
+
}
|
|
662
|
+
function measureJson(value) {
|
|
663
|
+
let values = 0;
|
|
664
|
+
let strings = 0;
|
|
665
|
+
const pending = [value];
|
|
666
|
+
while (pending.length > 0) {
|
|
667
|
+
const current = pending.pop();
|
|
668
|
+
values += 1;
|
|
669
|
+
if (typeof current === "string") {
|
|
670
|
+
strings += current.length;
|
|
671
|
+
}
|
|
672
|
+
else if (Array.isArray(current)) {
|
|
673
|
+
pending.push(...current);
|
|
674
|
+
}
|
|
675
|
+
else if (current !== null && typeof current === "object") {
|
|
676
|
+
for (const [key, child] of Object.entries(current)) {
|
|
677
|
+
strings += key.length;
|
|
678
|
+
pending.push(child);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return { values, strings };
|
|
683
|
+
}
|
|
684
|
+
function deepFreezeJson(value) {
|
|
685
|
+
const pending = [value];
|
|
686
|
+
const seen = new WeakSet();
|
|
687
|
+
while (pending.length > 0) {
|
|
688
|
+
const current = pending.pop();
|
|
689
|
+
if (current === null || typeof current !== "object" || seen.has(current)) {
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
seen.add(current);
|
|
693
|
+
if (Array.isArray(current)) {
|
|
694
|
+
pending.push(...current);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
pending.push(...Object.values(current));
|
|
698
|
+
}
|
|
699
|
+
Object.freeze(current);
|
|
700
|
+
}
|
|
701
|
+
return value;
|
|
702
|
+
}
|
|
703
|
+
function canonicalizeJson(value) {
|
|
704
|
+
if (value === null ||
|
|
705
|
+
typeof value === "boolean" ||
|
|
706
|
+
typeof value === "number" ||
|
|
707
|
+
typeof value === "string") {
|
|
708
|
+
return JSON.stringify(value);
|
|
709
|
+
}
|
|
710
|
+
if (Array.isArray(value)) {
|
|
711
|
+
return `[${value.map(canonicalizeJson).join(",")}]`;
|
|
712
|
+
}
|
|
713
|
+
const object = value;
|
|
714
|
+
return `{${Object.keys(object)
|
|
715
|
+
.sort()
|
|
716
|
+
.map((key) => `${JSON.stringify(key)}:${canonicalizeJson(object[key])}`)
|
|
717
|
+
.join(",")}}`;
|
|
718
|
+
}
|
|
719
|
+
//# sourceMappingURL=host-extensions.js.map
|