@mcp-native/react-native 0.6.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/dist/v1.js CHANGED
@@ -1,19 +1,53 @@
1
- import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
1
+ import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, validateA2uiV1HostExtensionComponent, validateA2uiV1SurfaceState, } from "@mcp-native/a2ui";
2
2
  import { JSON_MAX_STRING_LENGTH, JSON_MAX_VALUES, parseJsonObject, parseJsonValue, } from "@mcp-native/core";
3
3
  import { ISO_4217_CURRENCY_CODES } from "./iso-4217.js";
4
+ import { A2UI_V1_NATIVE_ICON_NAMES } from "./component-adapters.js";
4
5
  export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
6
+ "AudioPlayer",
5
7
  "Button",
6
8
  "Card",
9
+ "CheckBox",
10
+ "ChoicePicker",
7
11
  "Column",
12
+ "DateTimeInput",
13
+ "Divider",
14
+ "Icon",
15
+ "Image",
8
16
  "List",
17
+ "Modal",
9
18
  "Row",
19
+ "Slider",
20
+ "Tabs",
10
21
  "Text",
11
22
  "TextField",
23
+ "Video",
12
24
  ]);
13
25
  /** Maximum expanded native-plan nodes, including repeated component references. */
14
26
  export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
15
27
  /** Maximum canonical HTTP(S) URL retained for one supported local action. */
16
28
  export const A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH = 8_192;
29
+ /** Maximum canonical HTTP(S) image URL retained for one image component. */
30
+ export const A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH = 8_192;
31
+ /** Maximum choice options expanded into trusted host props in one render pass. */
32
+ export const A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS = 1_024;
33
+ export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS = 64;
34
+ export const A2UI_V1_NATIVE_MAX_IMAGE_BYTES = 104_857_600;
35
+ export const A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION = 16_384;
36
+ export const A2UI_V1_NATIVE_MAX_IMAGE_PIXELS = 268_435_456;
37
+ export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS = 10;
38
+ /** Maximum reachable Image instances in one expanded surface. Checked before host policy calls. */
39
+ export const A2UI_V1_NATIVE_MAX_IMAGES = 64;
40
+ /** Maximum sum of host-granted transfer bytes across one expanded render plan. */
41
+ export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES = A2UI_V1_NATIVE_MAX_IMAGE_BYTES;
42
+ /** Maximum sum of host-granted decoded pixels across one expanded render plan. */
43
+ export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS = A2UI_V1_NATIVE_MAX_IMAGE_PIXELS;
44
+ export const A2UI_V1_NATIVE_MAX_MEDIA = 16;
45
+ export const A2UI_V1_NATIVE_MAX_MEDIA_URL_LENGTH = 8_192;
46
+ export const A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES = 32;
47
+ export const A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS = 64;
48
+ export const A2UI_V1_NATIVE_MAX_MEDIA_REDIRECTS = 10;
49
+ export const A2UI_V1_NATIVE_MAX_MEDIA_BYTES = 2_147_483_648;
50
+ export const A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES = 2_147_483_648;
17
51
  /**
18
52
  * Converts a policy-validated A2UI v1 surface into the existing host-owned
19
53
  * native render plan. Unsupported renderer semantics fail closed.
@@ -27,7 +61,7 @@ export function createA2uiV1NativeRenderPlanForLocalEdits(surface, policy, optio
27
61
  }
28
62
  function createNativeRenderPlan(surface, policy, options, tolerateInvalidLocalOpenUrls) {
29
63
  const parsedOptions = parseRenderPlanOptions(options);
30
- const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls);
64
+ const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls, parsedOptions.imagePolicy, parsedOptions.mediaPolicy, parsedOptions.hostExtensionPolicy);
31
65
  return adaptComponent("root", "root", context, undefined);
32
66
  }
33
67
  /** Resolves one validated event against the latest renderer-local data model. */
@@ -36,7 +70,7 @@ export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dat
36
70
  throw new A2uiParseError("Expected a non-empty A2UI source component id");
37
71
  }
38
72
  const parsedOptions = parseEventResolutionOptions(options);
39
- const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
73
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, undefined, undefined, false);
40
74
  const plan = adaptComponent("root", "root", context, undefined);
41
75
  const events = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey);
42
76
  if (events.length === 0) {
@@ -57,7 +91,7 @@ export function resolveA2uiV1NativeOpenUrl(surface, policy, sourceComponentId, d
57
91
  throw new A2uiParseError("Expected a non-empty A2UI openUrl source component id");
58
92
  }
59
93
  const parsedOptions = parseOpenUrlResolutionOptions(options);
60
- const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
94
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, undefined, undefined, false);
61
95
  const plan = adaptComponent("root", "root", context, undefined);
62
96
  const openUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey);
63
97
  if (openUrls.length === 0) {
@@ -98,7 +132,7 @@ function findNativeOpenUrls(element, sourceComponentId, instanceKey, includeDisa
98
132
  }
99
133
  return openUrls;
100
134
  }
101
- function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false) {
135
+ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false, imagePolicy, mediaPolicy, hostExtensionPolicy, authorizeResources = true) {
102
136
  const localDataModel = dataModel === undefined
103
137
  ? parseJsonObject(surface.dataModel, "surface.dataModel")
104
138
  : parseJsonObject(dataModel, "options.dataModel");
@@ -112,9 +146,29 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
112
146
  pluralRules: new Map(),
113
147
  visiting: new Set(),
114
148
  tolerateInvalidLocalOpenUrls,
149
+ imagePolicy,
150
+ mediaPolicy,
151
+ hostExtensionPolicy,
152
+ hostExtensions: policy.hostExtensions,
153
+ authorizeResources,
154
+ choiceOptionCount: 0,
155
+ choiceOptionOutputLength: 0,
115
156
  formatStringExpressionCount: 0,
116
157
  formattedStringLength: 0,
117
158
  openUrlLength: 0,
159
+ imageUrlLength: 0,
160
+ imageCount: 0,
161
+ imageMaximumBytes: 0,
162
+ imageMaximumDecodedPixels: 0,
163
+ imageRedirectOriginCount: 0,
164
+ imageResourcePolicyOutputLength: 0,
165
+ mediaCount: 0,
166
+ mediaUrlLength: 0,
167
+ mediaMaximumBytes: 0,
168
+ mediaMimeTypeCount: 0,
169
+ mediaRedirectOriginCount: 0,
170
+ mediaPolicyOutputLength: 0,
171
+ hostExtensionCounts: new Map(),
118
172
  renderNodeCount: 0,
119
173
  validationCheckCount: 0,
120
174
  validationOutputLength: 0,
@@ -123,8 +177,14 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
123
177
  function parseRenderPlanOptions(options) {
124
178
  const parsed = parseOptionsObject(options, "A2UI native render plan options", [
125
179
  "dataModel",
180
+ "hostExtensionPolicy",
181
+ "imagePolicy",
126
182
  "locale",
183
+ "mediaPolicy",
127
184
  ]);
185
+ const imagePolicy = parseOptionalImagePolicy(parsed.imagePolicy, "options.imagePolicy");
186
+ const mediaPolicy = parseOptionalPolicy(parsed.mediaPolicy, "options.mediaPolicy");
187
+ const hostExtensionPolicy = parseOptionalPolicy(parsed.hostExtensionPolicy, "options.hostExtensionPolicy");
128
188
  return {
129
189
  ...(parsed.dataModel === undefined
130
190
  ? {}
@@ -132,6 +192,9 @@ function parseRenderPlanOptions(options) {
132
192
  ...(parsed.locale === undefined
133
193
  ? {}
134
194
  : { locale: parseLocale(parsed.locale, "options.locale") }),
195
+ ...(imagePolicy === undefined ? {} : { imagePolicy }),
196
+ ...(mediaPolicy === undefined ? {} : { mediaPolicy }),
197
+ ...(hostExtensionPolicy === undefined ? {} : { hostExtensionPolicy }),
135
198
  };
136
199
  }
137
200
  function parseEventResolutionOptions(options) {
@@ -166,6 +229,18 @@ function parseOpenUrlResolutionOptions(options) {
166
229
  : { locale: parseLocale(parsed.locale, "options.locale") }),
167
230
  };
168
231
  }
232
+ function parseOptionalImagePolicy(value, path) {
233
+ if (value !== undefined && typeof value !== "function") {
234
+ throw new A2uiParseError(`Expected a function at ${path}`);
235
+ }
236
+ return value;
237
+ }
238
+ function parseOptionalPolicy(value, path) {
239
+ if (value !== undefined && typeof value !== "function") {
240
+ throw new A2uiParseError(`Expected a function at ${path}`);
241
+ }
242
+ return value;
243
+ }
169
244
  function parseOptionsObject(value, label, allowedKeys) {
170
245
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
171
246
  throw new A2uiParseError(`Expected ${label} to be an object`);
@@ -227,12 +302,34 @@ function adaptComponent(id, key, context, scope) {
227
302
  return adaptCard(component, key, context, scope);
228
303
  case "Text":
229
304
  return adaptText(component, key, context, scope);
305
+ case "Image":
306
+ return adaptImage(component, key, context, scope);
307
+ case "Video":
308
+ return adaptMedia(component, key, "video", context, scope);
309
+ case "AudioPlayer":
310
+ return adaptMedia(component, key, "audio", context, scope);
311
+ case "Icon":
312
+ return adaptIcon(component, key, context, scope);
313
+ case "Divider":
314
+ return adaptDivider(component, key, context, scope);
230
315
  case "Button":
231
316
  return adaptButton(component, key, context, scope);
232
317
  case "TextField":
233
318
  return adaptTextField(component, key, context, scope);
319
+ case "CheckBox":
320
+ return adaptCheckBox(component, key, context, scope);
321
+ case "ChoicePicker":
322
+ return adaptChoicePicker(component, key, context, scope);
323
+ case "Slider":
324
+ return adaptSlider(component, key, context, scope);
325
+ case "DateTimeInput":
326
+ return adaptDateTimeInput(component, key, context, scope);
327
+ case "Tabs":
328
+ return adaptTabs(component, key, context, scope);
329
+ case "Modal":
330
+ return adaptModal(component, key, context, scope);
234
331
  default:
235
- throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
332
+ return adaptHostExtension(component, key, context, scope);
236
333
  }
237
334
  }
238
335
  finally {
@@ -317,6 +414,221 @@ function adaptText(component, key, context, scope) {
317
414
  addCommonProps(component, props, context, scope);
318
415
  return { key, component: "Text", props };
319
416
  }
417
+ function adaptImage(component, key, context, scope) {
418
+ context.imageCount += 1;
419
+ if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
420
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} images`);
421
+ }
422
+ const path = `components.${component.id}.url`;
423
+ const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, "image", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
424
+ context.imageUrlLength += url.length;
425
+ if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
426
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH} at ${path}`);
427
+ }
428
+ const props = {
429
+ uri: url,
430
+ fit: component.fit ?? "fill",
431
+ variant: component.variant ?? "mediumFeature",
432
+ };
433
+ if (context.authorizeResources) {
434
+ props.resourcePolicy = authorizeImageResource(component.id, key, url, `components.${component.id}.imagePolicy`, context);
435
+ }
436
+ if (component.description !== undefined) {
437
+ props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
438
+ }
439
+ addCommonProps(component, props, context, scope);
440
+ if (props.accessibilityLabel === undefined && props.description !== undefined) {
441
+ props.accessibilityLabel = props.description;
442
+ }
443
+ return { key, component: "Image", props };
444
+ }
445
+ function authorizeImageResource(componentId, key, url, path, context) {
446
+ const request = Object.freeze({
447
+ url,
448
+ surfaceId: context.surface.surfaceId,
449
+ sourceComponentId: componentId,
450
+ instanceKey: key,
451
+ });
452
+ if (context.imagePolicy === undefined) {
453
+ throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} requires an explicit host image policy`);
454
+ }
455
+ let decision;
456
+ try {
457
+ decision = context.imagePolicy(request);
458
+ }
459
+ catch (cause) {
460
+ throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} host policy failed`, { cause });
461
+ }
462
+ if (decision === false) {
463
+ throw new A2uiParseError(`A2UI image resource ${JSON.stringify(componentId)} URL is denied by host policy`);
464
+ }
465
+ const resourcePolicy = parseImageResourcePolicy(decision, path, context);
466
+ context.imageMaximumBytes += resourcePolicy.maximumBytes;
467
+ if (context.imageMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES) {
468
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total image transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES} bytes`);
469
+ }
470
+ context.imageMaximumDecodedPixels += resourcePolicy.maximumDecodedPixels;
471
+ if (context.imageMaximumDecodedPixels > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS) {
472
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total decoded image budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS} pixels`);
473
+ }
474
+ return resourcePolicy;
475
+ }
476
+ function adaptMedia(component, key, kind, context, scope) {
477
+ context.mediaCount += 1;
478
+ if (context.mediaCount > A2UI_V1_NATIVE_MAX_MEDIA) {
479
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_MEDIA} media components`);
480
+ }
481
+ const path = `components.${component.id}.url`;
482
+ const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, kind, A2UI_V1_NATIVE_MAX_MEDIA_URL_LENGTH);
483
+ context.mediaUrlLength += url.length;
484
+ if (context.mediaUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
485
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds cumulative media URL length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
486
+ }
487
+ const props = { uri: url };
488
+ if (context.authorizeResources) {
489
+ props.resourcePolicy = authorizeMediaResource(component.id, key, kind, url, context);
490
+ }
491
+ if (kind === "video" && component.posterUrl !== undefined) {
492
+ const posterPath = `components.${component.id}.posterUrl`;
493
+ const posterUrl = normalizeHttpUrl(resolveDynamicString(component.posterUrl, posterPath, context, scope), posterPath, "video poster", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
494
+ context.imageCount += 1;
495
+ if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
496
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} image resources`);
497
+ }
498
+ context.imageUrlLength += posterUrl.length;
499
+ if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
500
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds cumulative image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
501
+ }
502
+ props.posterUri = posterUrl;
503
+ if (context.authorizeResources) {
504
+ props.posterResourcePolicy = authorizeImageResource(component.id, `${key}:poster`, posterUrl, `components.${component.id}.posterPolicy`, context);
505
+ }
506
+ }
507
+ if (kind === "audio" && component.description !== undefined) {
508
+ props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
509
+ }
510
+ addCommonProps(component, props, context, scope);
511
+ if (props.accessibilityLabel === undefined) {
512
+ props.accessibilityLabel =
513
+ kind === "audio" && typeof props.description === "string"
514
+ ? props.description
515
+ : kind === "audio"
516
+ ? "Audio"
517
+ : "Video";
518
+ }
519
+ return { key, component: kind === "video" ? "Video" : "AudioPlayer", props };
520
+ }
521
+ function authorizeMediaResource(componentId, key, kind, url, context) {
522
+ if (context.mediaPolicy === undefined) {
523
+ throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} requires an explicit host media policy`);
524
+ }
525
+ const sourceOrigin = parseHttpUrlOrigin(url, `components.${componentId}.url`);
526
+ const request = Object.freeze({
527
+ kind,
528
+ url,
529
+ sourceOrigin,
530
+ surfaceId: context.surface.surfaceId,
531
+ sourceComponentId: componentId,
532
+ instanceKey: key,
533
+ });
534
+ let decision;
535
+ try {
536
+ decision = context.mediaPolicy(request);
537
+ }
538
+ catch (cause) {
539
+ throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} host policy failed`, {
540
+ cause,
541
+ });
542
+ }
543
+ if (decision === false) {
544
+ throw new A2uiParseError(`A2UI ${kind} ${JSON.stringify(componentId)} URL is denied by host policy`);
545
+ }
546
+ const resourcePolicy = parseMediaResourcePolicy(decision, request, `components.${componentId}.mediaPolicy`, context);
547
+ context.mediaMaximumBytes += resourcePolicy.maximumBytes;
548
+ if (context.mediaMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES) {
549
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total media transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_MEDIA_BYTES} bytes`);
550
+ }
551
+ return resourcePolicy;
552
+ }
553
+ function adaptHostExtension(component, key, context, scope) {
554
+ const hostExtensions = context.hostExtensions;
555
+ if (hostExtensions === undefined) {
556
+ throw new A2uiParseError(`A2UI component ${JSON.stringify(component.id)} uses an unavailable host extension`);
557
+ }
558
+ const validated = validateA2uiV1HostExtensionComponent(hostExtensions, component, `components.${component.id}`);
559
+ const manifest = validated.manifest;
560
+ const countKey = `${manifest.catalogId}\u0000${manifest.componentName}`;
561
+ const count = (context.hostExtensionCounts.get(countKey) ?? 0) + 1;
562
+ if (count > manifest.limits.maximumInstances) {
563
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${manifest.limits.maximumInstances} instances for ${JSON.stringify(manifest.componentName)}`);
564
+ }
565
+ context.hostExtensionCounts.set(countKey, count);
566
+ const props = {
567
+ extensionId: manifest.extensionId,
568
+ catalogId: manifest.catalogId,
569
+ schemaVersion: manifest.schemaVersion,
570
+ componentName: manifest.componentName,
571
+ manifestFingerprint: validated.manifestFingerprint,
572
+ semanticProps: validated.props,
573
+ sourceComponentId: component.id,
574
+ surfaceId: context.surface.surfaceId,
575
+ };
576
+ if (context.authorizeResources) {
577
+ if (context.hostExtensionPolicy === undefined) {
578
+ throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} requires an explicit capability policy`);
579
+ }
580
+ const request = Object.freeze({
581
+ extensionId: manifest.extensionId,
582
+ catalogId: manifest.catalogId,
583
+ schemaVersion: manifest.schemaVersion,
584
+ componentName: manifest.componentName,
585
+ surfaceId: context.surface.surfaceId,
586
+ sourceComponentId: component.id,
587
+ instanceKey: key,
588
+ platform: hostExtensions.platform,
589
+ semanticProps: validated.props,
590
+ permissionNeeds: manifest.permissionNeeds,
591
+ resourceNeeds: manifest.resourceNeeds,
592
+ });
593
+ let decision;
594
+ try {
595
+ decision = context.hostExtensionPolicy(request);
596
+ }
597
+ catch (cause) {
598
+ throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} capability policy failed`, { cause });
599
+ }
600
+ if (decision === false) {
601
+ throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} is denied by capability policy`);
602
+ }
603
+ props.capabilityGrant = parseHostExtensionCapabilityGrant(decision, manifest.permissionNeeds, manifest.resourceNeeds, `components.${component.id}.hostExtensionPolicy`);
604
+ }
605
+ addCommonProps(component, props, context, scope);
606
+ if (manifest.accessibility.requiresLabel && props.accessibilityLabel === undefined) {
607
+ throw new A2uiParseError(`Host extension ${JSON.stringify(manifest.componentName)} requires an accessibility label`);
608
+ }
609
+ return { key, component: "HostExtension", props };
610
+ }
611
+ function adaptIcon(component, key, context, scope) {
612
+ const path = `components.${component.id}.name`;
613
+ if (component.name !== null &&
614
+ typeof component.name === "object" &&
615
+ !Array.isArray(component.name) &&
616
+ Object.hasOwn(component.name, "svgPath")) {
617
+ throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
618
+ }
619
+ const name = resolveDynamicString(component.name, path, context, scope);
620
+ if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
621
+ throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
622
+ }
623
+ const props = { name };
624
+ addCommonProps(component, props, context, scope);
625
+ return { key, component: "Icon", props };
626
+ }
627
+ function adaptDivider(component, key, context, scope) {
628
+ const props = { axis: component.axis ?? "horizontal" };
629
+ addCommonProps(component, props, context, scope);
630
+ return { key, component: "Divider", props };
631
+ }
320
632
  function adaptButton(component, key, context, scope) {
321
633
  const childId = expectString(component.child, `components.${component.id}.child`);
322
634
  const child = context.surface.components.get(childId);
@@ -364,6 +676,244 @@ function adaptTextField(component, key, context, scope) {
364
676
  }
365
677
  return { key, component: "TextInput", props };
366
678
  }
679
+ function adaptCheckBox(component, key, context, scope) {
680
+ const componentPath = `components.${component.id}`;
681
+ const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
682
+ const props = {
683
+ label,
684
+ value: resolveDynamicBoolean(component.value, `${componentPath}.value`, context, scope),
685
+ };
686
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
687
+ addCommonProps(component, props, context, scope);
688
+ addValidationProps(component, props, context, scope, "input");
689
+ if (props.accessibilityLabel === undefined) {
690
+ props.accessibilityLabel = label;
691
+ }
692
+ return { key, component: "CheckBox", props };
693
+ }
694
+ function adaptChoicePicker(component, key, context, scope) {
695
+ const componentPath = `components.${component.id}`;
696
+ if (!Array.isArray(component.options)) {
697
+ throw new A2uiParseError(`Expected an array at ${componentPath}.options`);
698
+ }
699
+ context.choiceOptionCount += component.options.length;
700
+ if (context.choiceOptionCount > A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS) {
701
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS} choice options`);
702
+ }
703
+ const optionValues = new Set();
704
+ const options = component.options.map((value, index) => {
705
+ const option = expectObject(value, `${componentPath}.options[${index}]`);
706
+ const label = resolveDynamicString(option.label, `${componentPath}.options[${index}].label`, context, scope);
707
+ const optionValue = expectString(option.value, `${componentPath}.options[${index}].value`);
708
+ if (optionValues.has(optionValue)) {
709
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate option value ${JSON.stringify(optionValue)}`);
710
+ }
711
+ optionValues.add(optionValue);
712
+ context.choiceOptionOutputLength += label.length + optionValue.length;
713
+ if (context.choiceOptionOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
714
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum choice-option output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
715
+ }
716
+ return Object.freeze({ label, value: optionValue });
717
+ });
718
+ const selected = resolveDynamicStringList(component.value, `${componentPath}.value`, context, scope);
719
+ if (new Set(selected).size !== selected.length) {
720
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate selected values`);
721
+ }
722
+ for (const value of selected) {
723
+ if (!optionValues.has(value)) {
724
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} selects unknown value ${JSON.stringify(value)}`);
725
+ }
726
+ }
727
+ const variant = component.variant ?? "mutuallyExclusive";
728
+ if (variant === "mutuallyExclusive" && selected.length > 1) {
729
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} allows at most one selected value`);
730
+ }
731
+ const props = {
732
+ options: Object.freeze(options),
733
+ value: Object.freeze(selected),
734
+ variant,
735
+ displayStyle: component.displayStyle ?? "checkbox",
736
+ filterable: component.filterable ?? false,
737
+ };
738
+ if (component.label !== undefined) {
739
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
740
+ }
741
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
742
+ addCommonProps(component, props, context, scope);
743
+ addValidationProps(component, props, context, scope, "input");
744
+ if (props.accessibilityLabel === undefined) {
745
+ if (props.label === undefined) {
746
+ throw new A2uiParseError(`A2UI native ChoicePicker ${JSON.stringify(component.id)} requires label or accessibility.label`);
747
+ }
748
+ props.accessibilityLabel = props.label;
749
+ }
750
+ return { key, component: "ChoicePicker", props };
751
+ }
752
+ function adaptSlider(component, key, context, scope) {
753
+ const componentPath = `components.${component.id}`;
754
+ const minimum = component.min === undefined ? 0 : expectFiniteNumber(component.min, `${componentPath}.min`);
755
+ const maximum = expectFiniteNumber(component.max, `${componentPath}.max`);
756
+ if (minimum >= maximum) {
757
+ throw new A2uiParseError(`Expected ${componentPath}.min to be less than max`);
758
+ }
759
+ const value = resolveDynamicNumber(component.value, `${componentPath}.value`, context, scope);
760
+ if (value < minimum || value > maximum) {
761
+ throw new A2uiParseError(`Expected ${componentPath}.value to be within min and max`);
762
+ }
763
+ let step;
764
+ if (component.steps !== undefined) {
765
+ const steps = expectFiniteNumber(component.steps, `${componentPath}.steps`);
766
+ if (!Number.isInteger(steps) || steps < 1 || steps > JSON_MAX_VALUES) {
767
+ throw new A2uiParseError(`Expected ${componentPath}.steps to be an integer from 1 through ${JSON_MAX_VALUES}`);
768
+ }
769
+ step = (maximum - minimum) / steps;
770
+ if (!Number.isFinite(step) || step <= 0) {
771
+ throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} has invalid range`);
772
+ }
773
+ if (!isSliderStepValue(value, minimum, step)) {
774
+ throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} value does not align with its steps`);
775
+ }
776
+ }
777
+ const props = {
778
+ value,
779
+ minimum,
780
+ maximum,
781
+ ...(step === undefined ? {} : { step }),
782
+ };
783
+ if (component.label !== undefined) {
784
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
785
+ }
786
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
787
+ addCommonProps(component, props, context, scope);
788
+ addValidationProps(component, props, context, scope, "input");
789
+ if (props.accessibilityLabel === undefined) {
790
+ if (props.label === undefined) {
791
+ throw new A2uiParseError(`A2UI native Slider ${JSON.stringify(component.id)} requires label or accessibility.label`);
792
+ }
793
+ props.accessibilityLabel = props.label;
794
+ }
795
+ return { key, component: "Slider", props };
796
+ }
797
+ function isSliderStepValue(value, minimum, step) {
798
+ const offset = (value - minimum) / step;
799
+ return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
800
+ }
801
+ function adaptDateTimeInput(component, key, context, scope) {
802
+ const componentPath = `components.${component.id}`;
803
+ const enableDate = component.enableDate === true;
804
+ const enableTime = component.enableTime === true;
805
+ if (!enableDate && !enableTime) {
806
+ throw new A2uiParseError(`A2UI DateTimeInput ${JSON.stringify(component.id)} must enable date, time, or both`);
807
+ }
808
+ const value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
809
+ validateDateTimeInputValue(value, enableDate, enableTime, `${componentPath}.value`, true);
810
+ const minimum = component.min === undefined
811
+ ? undefined
812
+ : resolveDynamicString(component.min, `${componentPath}.min`, context, scope);
813
+ const maximum = component.max === undefined
814
+ ? undefined
815
+ : resolveDynamicString(component.max, `${componentPath}.max`, context, scope);
816
+ if (minimum !== undefined) {
817
+ validateDateTimeInputValue(minimum, enableDate, enableTime, `${componentPath}.min`, false);
818
+ }
819
+ if (maximum !== undefined) {
820
+ validateDateTimeInputValue(maximum, enableDate, enableTime, `${componentPath}.max`, false);
821
+ }
822
+ if (minimum !== undefined &&
823
+ maximum !== undefined &&
824
+ dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`) >
825
+ dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
826
+ throw new A2uiParseError(`Expected ${componentPath}.min not to exceed max`);
827
+ }
828
+ const valueSort = value.length === 0
829
+ ? undefined
830
+ : dateTimeInputSortValue(value, enableDate, enableTime, `${componentPath}.value`);
831
+ if (valueSort !== undefined &&
832
+ minimum !== undefined &&
833
+ valueSort < dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`)) {
834
+ throw new A2uiParseError(`Expected ${componentPath}.value not to be earlier than min`);
835
+ }
836
+ if (valueSort !== undefined &&
837
+ maximum !== undefined &&
838
+ valueSort > dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
839
+ throw new A2uiParseError(`Expected ${componentPath}.value not to be later than max`);
840
+ }
841
+ const props = {
842
+ value,
843
+ enableDate,
844
+ enableTime,
845
+ ...(minimum === undefined ? {} : { minimum }),
846
+ ...(maximum === undefined ? {} : { maximum }),
847
+ };
848
+ if (component.label !== undefined) {
849
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
850
+ }
851
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
852
+ addCommonProps(component, props, context, scope);
853
+ addValidationProps(component, props, context, scope, "input");
854
+ if (props.accessibilityLabel === undefined) {
855
+ if (props.label === undefined) {
856
+ throw new A2uiParseError(`A2UI native DateTimeInput ${JSON.stringify(component.id)} requires label or accessibility.label`);
857
+ }
858
+ props.accessibilityLabel = props.label;
859
+ }
860
+ return { key, component: "DateTimeInput", props };
861
+ }
862
+ function adaptTabs(component, key, context, scope) {
863
+ const componentPath = `components.${component.id}`;
864
+ if (!Array.isArray(component.tabs) || component.tabs.length === 0) {
865
+ throw new A2uiParseError(`Expected a non-empty array at ${componentPath}.tabs`);
866
+ }
867
+ const tabs = component.tabs.map((value, index) => {
868
+ const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
869
+ return Object.freeze({
870
+ title: resolveDynamicString(tab.title, `${componentPath}.tabs[${index}].title`, context, scope),
871
+ });
872
+ });
873
+ const props = { tabs: Object.freeze(tabs) };
874
+ addCommonProps(component, props, context, scope);
875
+ return {
876
+ key,
877
+ component: "Tabs",
878
+ props,
879
+ children: component.tabs.map((value, index) => {
880
+ const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
881
+ const childId = expectString(tab.child, `${componentPath}.tabs[${index}].child`);
882
+ return adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope);
883
+ }),
884
+ };
885
+ }
886
+ function adaptModal(component, key, context, scope) {
887
+ const componentPath = `components.${component.id}`;
888
+ const triggerId = expectString(component.trigger, `${componentPath}.trigger`);
889
+ const contentId = expectString(component.content, `${componentPath}.content`);
890
+ if (context.surface.components.get(triggerId)?.component !== "Button") {
891
+ throw new A2uiParseError(`A2UI native Modal ${JSON.stringify(component.id)} requires a Button trigger`);
892
+ }
893
+ const props = {};
894
+ addCommonProps(component, props, context, scope);
895
+ return {
896
+ key,
897
+ component: "Modal",
898
+ props,
899
+ children: [
900
+ adaptComponent(triggerId, appendInstanceKey(key, triggerId, 0), context, scope),
901
+ adaptComponent(contentId, appendInstanceKey(key, contentId, 1), context, scope),
902
+ ],
903
+ };
904
+ }
905
+ function addBindingProp(value, path, props, scope) {
906
+ if (value !== undefined && isBinding(value)) {
907
+ props.binding = resolveBindingPointer(value.path, `${path}.path`, scope);
908
+ }
909
+ }
910
+ function resolveDynamicStringList(value, path, context, scope) {
911
+ const resolved = resolveDynamicValue(value, path, context, scope);
912
+ if (!Array.isArray(resolved) || resolved.some((item) => typeof item !== "string")) {
913
+ throw new A2uiParseError(`Expected an array of strings at ${path}`);
914
+ }
915
+ return [...resolved];
916
+ }
367
917
  function addValidationProps(component, props, context, scope, target) {
368
918
  if (component.checks === undefined) {
369
919
  return;
@@ -608,15 +1158,18 @@ function resolveButtonAction(component, key, context, scope) {
608
1158
  };
609
1159
  }
610
1160
  function normalizeOpenUrl(value, path) {
611
- if (value.length === 0 || value.length > A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH) {
612
- throw new A2uiParseError(`Expected an HTTP(S) URL up to ${A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH} characters at ${path}`);
1161
+ return normalizeHttpUrl(value, path, "openUrl", A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH);
1162
+ }
1163
+ function normalizeHttpUrl(value, path, label, maximumLength) {
1164
+ if (value.length === 0 || value.length > maximumLength) {
1165
+ throw new A2uiParseError(`Expected an HTTP(S) URL up to ${maximumLength} characters at ${path}`);
613
1166
  }
614
1167
  if (/\s|\p{Cf}/u.test(value) || hasAsciiControlCharacter(value)) {
615
1168
  throw new A2uiParseError(`Expected an HTTP(S) URL without whitespace, control, or Unicode format characters at ${path}`);
616
1169
  }
617
1170
  const UrlConstructor = globalThis.URL;
618
1171
  if (UrlConstructor === undefined) {
619
- throw new A2uiParseError(`The host runtime cannot validate an openUrl value at ${path}`);
1172
+ throw new A2uiParseError(`The host runtime cannot validate an ${label} value at ${path}`);
620
1173
  }
621
1174
  let parsed;
622
1175
  try {
@@ -629,13 +1182,231 @@ function normalizeOpenUrl(value, path) {
629
1182
  throw new A2uiParseError(`Expected an HTTP(S) URL at ${path}`);
630
1183
  }
631
1184
  if (parsed.username.length > 0 || parsed.password.length > 0) {
632
- throw new A2uiParseError(`A2UI openUrl does not allow URL credentials at ${path}`);
1185
+ throw new A2uiParseError(`A2UI ${label} does not allow URL credentials at ${path}`);
633
1186
  }
634
- if (parsed.href.length > A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH) {
635
- throw new A2uiParseError(`Canonical A2UI openUrl at ${path} exceeds maximum length of ${A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH}`);
1187
+ if (parsed.href.length > maximumLength) {
1188
+ throw new A2uiParseError(`Canonical A2UI ${label} at ${path} exceeds maximum length of ${maximumLength}`);
636
1189
  }
637
1190
  return parsed.href;
638
1191
  }
1192
+ function parseImageResourcePolicy(value, path, context) {
1193
+ const policy = parseJsonObject(value, path);
1194
+ const allowedKeys = new Set([
1195
+ "allowedRedirectOrigins",
1196
+ "cacheMode",
1197
+ "maximumBytes",
1198
+ "maximumDecodedHeight",
1199
+ "maximumDecodedPixels",
1200
+ "maximumDecodedWidth",
1201
+ "maximumRedirects",
1202
+ ]);
1203
+ for (const key of Object.keys(policy)) {
1204
+ if (!allowedKeys.has(key)) {
1205
+ throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
1206
+ }
1207
+ }
1208
+ if (!Array.isArray(policy.allowedRedirectOrigins) ||
1209
+ policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS) {
1210
+ throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
1211
+ }
1212
+ context.imageRedirectOriginCount += policy.allowedRedirectOrigins.length;
1213
+ if (context.imageRedirectOriginCount > JSON_MAX_VALUES) {
1214
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} image redirect origins`);
1215
+ }
1216
+ const origins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
1217
+ if (new Set(origins).size !== origins.length) {
1218
+ throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
1219
+ }
1220
+ context.imageResourcePolicyOutputLength += origins.reduce((length, origin) => length + origin.length, 0);
1221
+ if (context.imageResourcePolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
1222
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
1223
+ }
1224
+ const cacheMode = policy.cacheMode;
1225
+ if (cacheMode !== "default" && cacheMode !== "no-store") {
1226
+ throw new A2uiParseError(`Expected a closed cache mode at ${path}.cacheMode`);
1227
+ }
1228
+ const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_IMAGE_BYTES, `${path}.maximumBytes`);
1229
+ const maximumDecodedWidth = parseBoundedPositiveInteger(policy.maximumDecodedWidth, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedWidth`);
1230
+ const maximumDecodedHeight = parseBoundedPositiveInteger(policy.maximumDecodedHeight, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedHeight`);
1231
+ const maximumDecodedPixels = parseBoundedPositiveInteger(policy.maximumDecodedPixels, A2UI_V1_NATIVE_MAX_IMAGE_PIXELS, `${path}.maximumDecodedPixels`);
1232
+ if (maximumDecodedPixels > maximumDecodedWidth * maximumDecodedHeight) {
1233
+ throw new A2uiParseError(`Expected ${path}.maximumDecodedPixels not to exceed the declared dimensions`);
1234
+ }
1235
+ const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS, `${path}.maximumRedirects`);
1236
+ return Object.freeze({
1237
+ allowedRedirectOrigins: Object.freeze(origins),
1238
+ cacheMode,
1239
+ maximumBytes,
1240
+ maximumDecodedHeight,
1241
+ maximumDecodedPixels,
1242
+ maximumDecodedWidth,
1243
+ maximumRedirects,
1244
+ });
1245
+ }
1246
+ function parseMediaResourcePolicy(value, request, path, context) {
1247
+ const policy = parseJsonObject(value, path);
1248
+ const allowedKeys = new Set([
1249
+ "allowedMimeTypes",
1250
+ "allowedRedirectOrigins",
1251
+ "allowsAutoplay",
1252
+ "allowsBackgroundPlayback",
1253
+ "allowsExternalRoutes",
1254
+ "maximumBytes",
1255
+ "maximumRedirects",
1256
+ "requiresUserActivation",
1257
+ "sourceOrigin",
1258
+ ]);
1259
+ for (const key of Object.keys(policy)) {
1260
+ if (!allowedKeys.has(key)) {
1261
+ throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
1262
+ }
1263
+ }
1264
+ const sourceOrigin = parseExactHttpOrigin(policy.sourceOrigin, `${path}.sourceOrigin`);
1265
+ if (sourceOrigin !== request.sourceOrigin) {
1266
+ throw new A2uiParseError(`Expected ${path}.sourceOrigin to match the requested media origin`);
1267
+ }
1268
+ if (!Array.isArray(policy.allowedRedirectOrigins) ||
1269
+ policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS) {
1270
+ throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_MEDIA_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
1271
+ }
1272
+ context.mediaRedirectOriginCount += policy.allowedRedirectOrigins.length;
1273
+ if (context.mediaRedirectOriginCount > JSON_MAX_VALUES) {
1274
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} media redirect origins`);
1275
+ }
1276
+ const allowedRedirectOrigins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
1277
+ if (new Set(allowedRedirectOrigins).size !== allowedRedirectOrigins.length) {
1278
+ throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
1279
+ }
1280
+ if (!Array.isArray(policy.allowedMimeTypes) ||
1281
+ policy.allowedMimeTypes.length === 0 ||
1282
+ policy.allowedMimeTypes.length > A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES) {
1283
+ throw new A2uiParseError(`Expected one through ${A2UI_V1_NATIVE_MAX_MEDIA_MIME_TYPES} MIME types at ${path}.allowedMimeTypes`);
1284
+ }
1285
+ context.mediaMimeTypeCount += policy.allowedMimeTypes.length;
1286
+ if (context.mediaMimeTypeCount > JSON_MAX_VALUES) {
1287
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} media MIME types`);
1288
+ }
1289
+ const mimePrefix = request.kind === "video" ? "video/" : "audio/";
1290
+ const allowedMimeTypes = policy.allowedMimeTypes.map((mimeType, index) => {
1291
+ const parsed = expectString(mimeType, `${path}.allowedMimeTypes[${index}]`);
1292
+ if (parsed !== parsed.toLowerCase() ||
1293
+ !parsed.startsWith(mimePrefix) ||
1294
+ !/^(?:audio|video)\/[a-z0-9][a-z0-9!#$&^_.+-]*$/.test(parsed)) {
1295
+ throw new A2uiParseError(`Expected an exact lower-case ${request.kind} MIME type at ${path}.allowedMimeTypes[${index}]`);
1296
+ }
1297
+ return parsed;
1298
+ });
1299
+ if (new Set(allowedMimeTypes).size !== allowedMimeTypes.length) {
1300
+ throw new A2uiParseError(`Expected unique MIME types at ${path}.allowedMimeTypes`);
1301
+ }
1302
+ context.mediaPolicyOutputLength +=
1303
+ sourceOrigin.length +
1304
+ allowedRedirectOrigins.reduce((total, origin) => total + origin.length, 0) +
1305
+ allowedMimeTypes.reduce((total, mimeType) => total + mimeType.length, 0);
1306
+ if (context.mediaPolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
1307
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds media-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
1308
+ }
1309
+ const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_MEDIA_BYTES, `${path}.maximumBytes`);
1310
+ const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_MEDIA_REDIRECTS, `${path}.maximumRedirects`);
1311
+ const allowsAutoplay = parsePolicyBoolean(policy.allowsAutoplay, `${path}.allowsAutoplay`);
1312
+ const allowsBackgroundPlayback = parsePolicyBoolean(policy.allowsBackgroundPlayback, `${path}.allowsBackgroundPlayback`);
1313
+ const allowsExternalRoutes = parsePolicyBoolean(policy.allowsExternalRoutes, `${path}.allowsExternalRoutes`);
1314
+ const requiresUserActivation = parsePolicyBoolean(policy.requiresUserActivation, `${path}.requiresUserActivation`);
1315
+ if (allowsAutoplay && requiresUserActivation) {
1316
+ throw new A2uiParseError(`A2UI media policy cannot allow autoplay while requiring user activation at ${path}`);
1317
+ }
1318
+ return Object.freeze({
1319
+ sourceOrigin,
1320
+ allowedRedirectOrigins: Object.freeze(allowedRedirectOrigins),
1321
+ allowedMimeTypes: Object.freeze(allowedMimeTypes),
1322
+ maximumBytes,
1323
+ maximumRedirects,
1324
+ allowsAutoplay,
1325
+ allowsBackgroundPlayback,
1326
+ allowsExternalRoutes,
1327
+ requiresUserActivation,
1328
+ });
1329
+ }
1330
+ function parseHostExtensionCapabilityGrant(value, permissionNeeds, resourceNeeds, path) {
1331
+ const grant = parseJsonObject(value, path);
1332
+ const keys = Object.keys(grant);
1333
+ if (keys.some((key) => key !== "permissions" && key !== "resources")) {
1334
+ throw new A2uiParseError(`Unexpected host-extension capability grant field at ${path}`);
1335
+ }
1336
+ const permissions = parseExactGrantNames(grant.permissions, permissionNeeds, `${path}.permissions`);
1337
+ const resources = parseExactGrantNames(grant.resources, resourceNeeds, `${path}.resources`);
1338
+ return Object.freeze({ permissions, resources });
1339
+ }
1340
+ function parseExactGrantNames(value, expected, path) {
1341
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== "string")) {
1342
+ throw new A2uiParseError(`Expected an array of capability identifiers at ${path}`);
1343
+ }
1344
+ const names = value;
1345
+ if (new Set(names).size !== names.length) {
1346
+ throw new A2uiParseError(`Expected unique capability identifiers at ${path}`);
1347
+ }
1348
+ const actualSet = new Set(names);
1349
+ if (actualSet.size !== expected.length || expected.some((name) => !actualSet.has(name))) {
1350
+ throw new A2uiParseError(`Expected ${path} to exactly grant the manifest-declared needs`);
1351
+ }
1352
+ return Object.freeze([...expected]);
1353
+ }
1354
+ function parsePolicyBoolean(value, path) {
1355
+ if (typeof value !== "boolean") {
1356
+ throw new A2uiParseError(`Expected a boolean at ${path}`);
1357
+ }
1358
+ return value;
1359
+ }
1360
+ function parseHttpUrlOrigin(value, path) {
1361
+ const UrlConstructor = globalThis.URL;
1362
+ if (UrlConstructor === undefined) {
1363
+ throw new A2uiParseError(`The host runtime cannot validate a media origin at ${path}`);
1364
+ }
1365
+ try {
1366
+ return new UrlConstructor(value).origin;
1367
+ }
1368
+ catch (cause) {
1369
+ throw new A2uiParseError(`Expected an HTTP(S) media URL at ${path}`, { cause });
1370
+ }
1371
+ }
1372
+ function parseExactHttpOrigin(value, path) {
1373
+ const origin = expectString(value, path);
1374
+ if (/\s|\p{Cf}/u.test(origin) || hasAsciiControlCharacter(origin)) {
1375
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
1376
+ }
1377
+ const UrlConstructor = globalThis.URL;
1378
+ if (UrlConstructor === undefined) {
1379
+ throw new A2uiParseError(`The host runtime cannot validate a resource origin at ${path}`);
1380
+ }
1381
+ let parsed;
1382
+ try {
1383
+ parsed = new UrlConstructor(origin);
1384
+ }
1385
+ catch (cause) {
1386
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`, { cause });
1387
+ }
1388
+ if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
1389
+ parsed.username.length > 0 ||
1390
+ parsed.password.length > 0 ||
1391
+ parsed.origin !== origin) {
1392
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
1393
+ }
1394
+ return parsed.origin;
1395
+ }
1396
+ function parseBoundedPositiveInteger(value, maximum, path) {
1397
+ const result = expectFiniteNumber(value, path);
1398
+ if (!Number.isInteger(result) || result < 1 || result > maximum) {
1399
+ throw new A2uiParseError(`Expected an integer from 1 through ${maximum} at ${path}`);
1400
+ }
1401
+ return result;
1402
+ }
1403
+ function parseBoundedNonNegativeInteger(value, maximum, path) {
1404
+ const result = expectFiniteNumber(value, path);
1405
+ if (!Number.isInteger(result) || result < 0 || result > maximum) {
1406
+ throw new A2uiParseError(`Expected an integer from 0 through ${maximum} at ${path}`);
1407
+ }
1408
+ return result;
1409
+ }
639
1410
  function hasAsciiControlCharacter(value) {
640
1411
  for (let index = 0; index < value.length; index += 1) {
641
1412
  const code = value.charCodeAt(index);
@@ -803,6 +1574,7 @@ function resolveNumberFormat(call, path, context, scope) {
803
1574
  }
804
1575
  const DATE_NUMBER = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
805
1576
  const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
1577
+ const TIME_ONLY = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|([+-])(\d{2}):(\d{2}))?$/;
806
1578
  const RFC_3339_TIMESTAMP = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?(Z|([+-])(\d{2}):(\d{2}))$/;
807
1579
  const A2UI_V1_MAX_DATE_PATTERN_TOKENS = 128;
808
1580
  const DATE_PATTERN_TOKENS = Object.freeze([
@@ -825,6 +1597,98 @@ const DATE_PATTERN_TOKENS = Object.freeze([
825
1597
  "a",
826
1598
  ]);
827
1599
  const DATE_PATTERN_TOKEN_SET = new Set(DATE_PATTERN_TOKENS);
1600
+ function validateDateTimeInputValue(value, enableDate, enableTime, path, allowEmpty) {
1601
+ if (allowEmpty && value.length === 0) {
1602
+ return;
1603
+ }
1604
+ if (enableDate && enableTime) {
1605
+ parseDateValue(value, path);
1606
+ if (RFC_3339_TIMESTAMP.exec(value) === null) {
1607
+ throw new A2uiParseError(`Expected an RFC 3339 date-time at ${path}`);
1608
+ }
1609
+ return;
1610
+ }
1611
+ if (enableDate) {
1612
+ const match = DATE_ONLY.exec(value);
1613
+ if (match === null) {
1614
+ throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
1615
+ }
1616
+ validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
1617
+ return;
1618
+ }
1619
+ parseTimeOnly(value, path);
1620
+ }
1621
+ /** Internal mounted-renderer guard for values emitted by a trusted host date/time component. */
1622
+ export function validateA2uiV1NativeDateTimeInputChange(value, enableDate, enableTime, minimum, maximum, path) {
1623
+ if (!enableDate && !enableTime) {
1624
+ throw new A2uiParseError(`A2UI DateTimeInput at ${path} must enable date, time, or both`);
1625
+ }
1626
+ validateDateTimeInputValue(value, enableDate, enableTime, `${path}.value`, true);
1627
+ if (minimum !== undefined) {
1628
+ validateDateTimeInputValue(minimum, enableDate, enableTime, `${path}.minimum`, false);
1629
+ }
1630
+ if (maximum !== undefined) {
1631
+ validateDateTimeInputValue(maximum, enableDate, enableTime, `${path}.maximum`, false);
1632
+ }
1633
+ const minimumSort = minimum === undefined
1634
+ ? undefined
1635
+ : dateTimeInputSortValue(minimum, enableDate, enableTime, `${path}.minimum`);
1636
+ const maximumSort = maximum === undefined
1637
+ ? undefined
1638
+ : dateTimeInputSortValue(maximum, enableDate, enableTime, `${path}.maximum`);
1639
+ if (minimumSort !== undefined && maximumSort !== undefined && minimumSort > maximumSort) {
1640
+ throw new A2uiParseError(`Expected ${path}.minimum not to exceed maximum`);
1641
+ }
1642
+ if (value.length === 0) {
1643
+ return;
1644
+ }
1645
+ const valueSort = dateTimeInputSortValue(value, enableDate, enableTime, `${path}.value`);
1646
+ if (minimumSort !== undefined && valueSort < minimumSort) {
1647
+ throw new A2uiParseError(`Expected ${path}.value not to be earlier than minimum`);
1648
+ }
1649
+ if (maximumSort !== undefined && valueSort > maximumSort) {
1650
+ throw new A2uiParseError(`Expected ${path}.value not to be later than maximum`);
1651
+ }
1652
+ }
1653
+ function dateTimeInputSortValue(value, enableDate, enableTime, path) {
1654
+ if (enableDate && enableTime) {
1655
+ return parseDateValue(value, path).getTime();
1656
+ }
1657
+ if (enableDate) {
1658
+ const match = DATE_ONLY.exec(value);
1659
+ if (match === null) {
1660
+ throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
1661
+ }
1662
+ validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
1663
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
1664
+ }
1665
+ return parseTimeOnly(value, path);
1666
+ }
1667
+ function parseTimeOnly(value, path) {
1668
+ const match = TIME_ONLY.exec(value);
1669
+ if (match === null) {
1670
+ throw new A2uiParseError(`Expected an ISO 8601 time at ${path}`);
1671
+ }
1672
+ const hour = Number(match[1]);
1673
+ const minute = Number(match[2]);
1674
+ const second = Number(match[3] ?? "0");
1675
+ if (hour > 23 || minute > 59 || second > 59) {
1676
+ throw new A2uiParseError(`Invalid ISO 8601 time at ${path}`);
1677
+ }
1678
+ const fraction = match[4] ?? "";
1679
+ const millisecond = Number(fraction.slice(0, 3).padEnd(3, "0"));
1680
+ let result = ((hour * 60 + minute) * 60 + second) * 1_000 + millisecond;
1681
+ if (match[5] !== undefined && match[5] !== "Z") {
1682
+ const offsetHour = Number(match[7]);
1683
+ const offsetMinute = Number(match[8]);
1684
+ if (offsetHour > 23 || offsetMinute > 59) {
1685
+ throw new A2uiParseError(`Invalid ISO 8601 time offset at ${path}`);
1686
+ }
1687
+ const direction = match[6] === "+" ? 1 : -1;
1688
+ result -= direction * (offsetHour * 60 + offsetMinute) * 60_000;
1689
+ }
1690
+ return result;
1691
+ }
828
1692
  function resolveDateFormat(call, path, context, scope) {
829
1693
  const args = expectObject(call.args, `${path}.args`);
830
1694
  const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);