@mcp-native/react-native 0.6.0 → 0.7.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,12 +1,22 @@
1
1
  import { A2UI_V1_MAX_COMPONENTS, A2UI_V1_MAX_SOURCE_LENGTH, A2uiParseError, evaluateA2uiV1FormatString, 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([
5
6
  "Button",
6
7
  "Card",
8
+ "CheckBox",
9
+ "ChoicePicker",
7
10
  "Column",
11
+ "DateTimeInput",
12
+ "Divider",
13
+ "Icon",
14
+ "Image",
8
15
  "List",
16
+ "Modal",
9
17
  "Row",
18
+ "Slider",
19
+ "Tabs",
10
20
  "Text",
11
21
  "TextField",
12
22
  ]);
@@ -14,6 +24,21 @@ export const A2UI_V1_NATIVE_COMPONENT_NAMES = Object.freeze([
14
24
  export const A2UI_V1_NATIVE_MAX_RENDER_NODES = A2UI_V1_MAX_COMPONENTS;
15
25
  /** Maximum canonical HTTP(S) URL retained for one supported local action. */
16
26
  export const A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH = 8_192;
27
+ /** Maximum canonical HTTP(S) image URL retained for one image component. */
28
+ export const A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH = 8_192;
29
+ /** Maximum choice options expanded into trusted host props in one render pass. */
30
+ export const A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS = 1_024;
31
+ export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS = 64;
32
+ export const A2UI_V1_NATIVE_MAX_IMAGE_BYTES = 104_857_600;
33
+ export const A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION = 16_384;
34
+ export const A2UI_V1_NATIVE_MAX_IMAGE_PIXELS = 268_435_456;
35
+ export const A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS = 10;
36
+ /** Maximum reachable Image instances in one expanded surface. Checked before host policy calls. */
37
+ export const A2UI_V1_NATIVE_MAX_IMAGES = 64;
38
+ /** Maximum sum of host-granted transfer bytes across one expanded render plan. */
39
+ export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES = A2UI_V1_NATIVE_MAX_IMAGE_BYTES;
40
+ /** Maximum sum of host-granted decoded pixels across one expanded render plan. */
41
+ export const A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS = A2UI_V1_NATIVE_MAX_IMAGE_PIXELS;
17
42
  /**
18
43
  * Converts a policy-validated A2UI v1 surface into the existing host-owned
19
44
  * native render plan. Unsupported renderer semantics fail closed.
@@ -27,7 +52,7 @@ export function createA2uiV1NativeRenderPlanForLocalEdits(surface, policy, optio
27
52
  }
28
53
  function createNativeRenderPlan(surface, policy, options, tolerateInvalidLocalOpenUrls) {
29
54
  const parsedOptions = parseRenderPlanOptions(options);
30
- const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls);
55
+ const context = createAdapterContext(surface, policy, parsedOptions.dataModel, parsedOptions.locale, tolerateInvalidLocalOpenUrls, parsedOptions.imagePolicy);
31
56
  return adaptComponent("root", "root", context, undefined);
32
57
  }
33
58
  /** Resolves one validated event against the latest renderer-local data model. */
@@ -36,7 +61,7 @@ export function resolveA2uiV1NativeEvent(surface, policy, sourceComponentId, dat
36
61
  throw new A2uiParseError("Expected a non-empty A2UI source component id");
37
62
  }
38
63
  const parsedOptions = parseEventResolutionOptions(options);
39
- const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
64
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, false);
40
65
  const plan = adaptComponent("root", "root", context, undefined);
41
66
  const events = findNativeEvents(plan, sourceComponentId, parsedOptions.instanceKey);
42
67
  if (events.length === 0) {
@@ -57,7 +82,7 @@ export function resolveA2uiV1NativeOpenUrl(surface, policy, sourceComponentId, d
57
82
  throw new A2uiParseError("Expected a non-empty A2UI openUrl source component id");
58
83
  }
59
84
  const parsedOptions = parseOpenUrlResolutionOptions(options);
60
- const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale);
85
+ const context = createAdapterContext(surface, policy, dataModel, parsedOptions.locale, false, undefined, false);
61
86
  const plan = adaptComponent("root", "root", context, undefined);
62
87
  const openUrls = findNativeOpenUrls(plan, sourceComponentId, parsedOptions.instanceKey);
63
88
  if (openUrls.length === 0) {
@@ -98,7 +123,7 @@ function findNativeOpenUrls(element, sourceComponentId, instanceKey, includeDisa
98
123
  }
99
124
  return openUrls;
100
125
  }
101
- function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false) {
126
+ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvalidLocalOpenUrls = false, imagePolicy, authorizeImageResources = true) {
102
127
  const localDataModel = dataModel === undefined
103
128
  ? parseJsonObject(surface.dataModel, "surface.dataModel")
104
129
  : parseJsonObject(dataModel, "options.dataModel");
@@ -112,9 +137,19 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
112
137
  pluralRules: new Map(),
113
138
  visiting: new Set(),
114
139
  tolerateInvalidLocalOpenUrls,
140
+ imagePolicy,
141
+ authorizeImageResources,
142
+ choiceOptionCount: 0,
143
+ choiceOptionOutputLength: 0,
115
144
  formatStringExpressionCount: 0,
116
145
  formattedStringLength: 0,
117
146
  openUrlLength: 0,
147
+ imageUrlLength: 0,
148
+ imageCount: 0,
149
+ imageMaximumBytes: 0,
150
+ imageMaximumDecodedPixels: 0,
151
+ imageRedirectOriginCount: 0,
152
+ imageResourcePolicyOutputLength: 0,
118
153
  renderNodeCount: 0,
119
154
  validationCheckCount: 0,
120
155
  validationOutputLength: 0,
@@ -123,8 +158,10 @@ function createAdapterContext(surface, policy, dataModel, locale, tolerateInvali
123
158
  function parseRenderPlanOptions(options) {
124
159
  const parsed = parseOptionsObject(options, "A2UI native render plan options", [
125
160
  "dataModel",
161
+ "imagePolicy",
126
162
  "locale",
127
163
  ]);
164
+ const imagePolicy = parseOptionalImagePolicy(parsed.imagePolicy, "options.imagePolicy");
128
165
  return {
129
166
  ...(parsed.dataModel === undefined
130
167
  ? {}
@@ -132,6 +169,7 @@ function parseRenderPlanOptions(options) {
132
169
  ...(parsed.locale === undefined
133
170
  ? {}
134
171
  : { locale: parseLocale(parsed.locale, "options.locale") }),
172
+ ...(imagePolicy === undefined ? {} : { imagePolicy }),
135
173
  };
136
174
  }
137
175
  function parseEventResolutionOptions(options) {
@@ -166,6 +204,12 @@ function parseOpenUrlResolutionOptions(options) {
166
204
  : { locale: parseLocale(parsed.locale, "options.locale") }),
167
205
  };
168
206
  }
207
+ function parseOptionalImagePolicy(value, path) {
208
+ if (value !== undefined && typeof value !== "function") {
209
+ throw new A2uiParseError(`Expected a function at ${path}`);
210
+ }
211
+ return value;
212
+ }
169
213
  function parseOptionsObject(value, label, allowedKeys) {
170
214
  if (value === null || typeof value !== "object" || Array.isArray(value)) {
171
215
  throw new A2uiParseError(`Expected ${label} to be an object`);
@@ -227,10 +271,28 @@ function adaptComponent(id, key, context, scope) {
227
271
  return adaptCard(component, key, context, scope);
228
272
  case "Text":
229
273
  return adaptText(component, key, context, scope);
274
+ case "Image":
275
+ return adaptImage(component, key, context, scope);
276
+ case "Icon":
277
+ return adaptIcon(component, key, context, scope);
278
+ case "Divider":
279
+ return adaptDivider(component, key, context, scope);
230
280
  case "Button":
231
281
  return adaptButton(component, key, context, scope);
232
282
  case "TextField":
233
283
  return adaptTextField(component, key, context, scope);
284
+ case "CheckBox":
285
+ return adaptCheckBox(component, key, context, scope);
286
+ case "ChoicePicker":
287
+ return adaptChoicePicker(component, key, context, scope);
288
+ case "Slider":
289
+ return adaptSlider(component, key, context, scope);
290
+ case "DateTimeInput":
291
+ return adaptDateTimeInput(component, key, context, scope);
292
+ case "Tabs":
293
+ return adaptTabs(component, key, context, scope);
294
+ case "Modal":
295
+ return adaptModal(component, key, context, scope);
234
296
  default:
235
297
  throw new A2uiParseError(`A2UI component ${JSON.stringify(id)} uses ${JSON.stringify(component.component)}, which the native adapter does not support`);
236
298
  }
@@ -317,6 +379,85 @@ function adaptText(component, key, context, scope) {
317
379
  addCommonProps(component, props, context, scope);
318
380
  return { key, component: "Text", props };
319
381
  }
382
+ function adaptImage(component, key, context, scope) {
383
+ context.imageCount += 1;
384
+ if (context.imageCount > A2UI_V1_NATIVE_MAX_IMAGES) {
385
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_IMAGES} images`);
386
+ }
387
+ const path = `components.${component.id}.url`;
388
+ const url = normalizeHttpUrl(resolveDynamicString(component.url, path, context, scope), path, "image", A2UI_V1_NATIVE_MAX_IMAGE_URL_LENGTH);
389
+ context.imageUrlLength += url.length;
390
+ if (context.imageUrlLength > A2UI_V1_MAX_SOURCE_LENGTH) {
391
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image URL length of ${A2UI_V1_MAX_SOURCE_LENGTH} at ${path}`);
392
+ }
393
+ const props = {
394
+ uri: url,
395
+ fit: component.fit ?? "fill",
396
+ variant: component.variant ?? "mediumFeature",
397
+ };
398
+ if (context.authorizeImageResources) {
399
+ const request = {
400
+ url,
401
+ surfaceId: context.surface.surfaceId,
402
+ sourceComponentId: component.id,
403
+ instanceKey: key,
404
+ };
405
+ if (context.imagePolicy === undefined) {
406
+ throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} requires an explicit host image policy`);
407
+ }
408
+ let decision;
409
+ try {
410
+ decision = context.imagePolicy(request);
411
+ }
412
+ catch (cause) {
413
+ throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} host policy failed`, {
414
+ cause,
415
+ });
416
+ }
417
+ if (decision === false) {
418
+ throw new A2uiParseError(`A2UI Image ${JSON.stringify(component.id)} URL is denied by host policy`);
419
+ }
420
+ const resourcePolicy = parseImageResourcePolicy(decision, `components.${component.id}.imagePolicy`, context);
421
+ context.imageMaximumBytes += resourcePolicy.maximumBytes;
422
+ if (context.imageMaximumBytes > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES) {
423
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total image transfer budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_BYTES} bytes`);
424
+ }
425
+ context.imageMaximumDecodedPixels += resourcePolicy.maximumDecodedPixels;
426
+ if (context.imageMaximumDecodedPixels > A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS) {
427
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum total decoded image budget of ${A2UI_V1_NATIVE_MAX_TOTAL_IMAGE_PIXELS} pixels`);
428
+ }
429
+ props.resourcePolicy = resourcePolicy;
430
+ }
431
+ if (component.description !== undefined) {
432
+ props.description = resolveDynamicString(component.description, `components.${component.id}.description`, context, scope);
433
+ }
434
+ addCommonProps(component, props, context, scope);
435
+ if (props.accessibilityLabel === undefined && props.description !== undefined) {
436
+ props.accessibilityLabel = props.description;
437
+ }
438
+ return { key, component: "Image", props };
439
+ }
440
+ function adaptIcon(component, key, context, scope) {
441
+ const path = `components.${component.id}.name`;
442
+ if (component.name !== null &&
443
+ typeof component.name === "object" &&
444
+ !Array.isArray(component.name) &&
445
+ Object.hasOwn(component.name, "svgPath")) {
446
+ throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
447
+ }
448
+ const name = resolveDynamicString(component.name, path, context, scope);
449
+ if (!A2UI_V1_NATIVE_ICON_NAMES.includes(name)) {
450
+ throw new A2uiParseError(`A2UI native Icon ${JSON.stringify(component.id)} requires a pinned semantic icon name at ${path}; svgPath is not supported`);
451
+ }
452
+ const props = { name };
453
+ addCommonProps(component, props, context, scope);
454
+ return { key, component: "Icon", props };
455
+ }
456
+ function adaptDivider(component, key, context, scope) {
457
+ const props = { axis: component.axis ?? "horizontal" };
458
+ addCommonProps(component, props, context, scope);
459
+ return { key, component: "Divider", props };
460
+ }
320
461
  function adaptButton(component, key, context, scope) {
321
462
  const childId = expectString(component.child, `components.${component.id}.child`);
322
463
  const child = context.surface.components.get(childId);
@@ -364,6 +505,244 @@ function adaptTextField(component, key, context, scope) {
364
505
  }
365
506
  return { key, component: "TextInput", props };
366
507
  }
508
+ function adaptCheckBox(component, key, context, scope) {
509
+ const componentPath = `components.${component.id}`;
510
+ const label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
511
+ const props = {
512
+ label,
513
+ value: resolveDynamicBoolean(component.value, `${componentPath}.value`, context, scope),
514
+ };
515
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
516
+ addCommonProps(component, props, context, scope);
517
+ addValidationProps(component, props, context, scope, "input");
518
+ if (props.accessibilityLabel === undefined) {
519
+ props.accessibilityLabel = label;
520
+ }
521
+ return { key, component: "CheckBox", props };
522
+ }
523
+ function adaptChoicePicker(component, key, context, scope) {
524
+ const componentPath = `components.${component.id}`;
525
+ if (!Array.isArray(component.options)) {
526
+ throw new A2uiParseError(`Expected an array at ${componentPath}.options`);
527
+ }
528
+ context.choiceOptionCount += component.options.length;
529
+ if (context.choiceOptionCount > A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS) {
530
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${A2UI_V1_NATIVE_MAX_CHOICE_OPTIONS} choice options`);
531
+ }
532
+ const optionValues = new Set();
533
+ const options = component.options.map((value, index) => {
534
+ const option = expectObject(value, `${componentPath}.options[${index}]`);
535
+ const label = resolveDynamicString(option.label, `${componentPath}.options[${index}].label`, context, scope);
536
+ const optionValue = expectString(option.value, `${componentPath}.options[${index}].value`);
537
+ if (optionValues.has(optionValue)) {
538
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate option value ${JSON.stringify(optionValue)}`);
539
+ }
540
+ optionValues.add(optionValue);
541
+ context.choiceOptionOutputLength += label.length + optionValue.length;
542
+ if (context.choiceOptionOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
543
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum choice-option output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
544
+ }
545
+ return Object.freeze({ label, value: optionValue });
546
+ });
547
+ const selected = resolveDynamicStringList(component.value, `${componentPath}.value`, context, scope);
548
+ if (new Set(selected).size !== selected.length) {
549
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} contains duplicate selected values`);
550
+ }
551
+ for (const value of selected) {
552
+ if (!optionValues.has(value)) {
553
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} selects unknown value ${JSON.stringify(value)}`);
554
+ }
555
+ }
556
+ const variant = component.variant ?? "mutuallyExclusive";
557
+ if (variant === "mutuallyExclusive" && selected.length > 1) {
558
+ throw new A2uiParseError(`A2UI ChoicePicker ${JSON.stringify(component.id)} allows at most one selected value`);
559
+ }
560
+ const props = {
561
+ options: Object.freeze(options),
562
+ value: Object.freeze(selected),
563
+ variant,
564
+ displayStyle: component.displayStyle ?? "checkbox",
565
+ filterable: component.filterable ?? false,
566
+ };
567
+ if (component.label !== undefined) {
568
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
569
+ }
570
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
571
+ addCommonProps(component, props, context, scope);
572
+ addValidationProps(component, props, context, scope, "input");
573
+ if (props.accessibilityLabel === undefined) {
574
+ if (props.label === undefined) {
575
+ throw new A2uiParseError(`A2UI native ChoicePicker ${JSON.stringify(component.id)} requires label or accessibility.label`);
576
+ }
577
+ props.accessibilityLabel = props.label;
578
+ }
579
+ return { key, component: "ChoicePicker", props };
580
+ }
581
+ function adaptSlider(component, key, context, scope) {
582
+ const componentPath = `components.${component.id}`;
583
+ const minimum = component.min === undefined ? 0 : expectFiniteNumber(component.min, `${componentPath}.min`);
584
+ const maximum = expectFiniteNumber(component.max, `${componentPath}.max`);
585
+ if (minimum >= maximum) {
586
+ throw new A2uiParseError(`Expected ${componentPath}.min to be less than max`);
587
+ }
588
+ const value = resolveDynamicNumber(component.value, `${componentPath}.value`, context, scope);
589
+ if (value < minimum || value > maximum) {
590
+ throw new A2uiParseError(`Expected ${componentPath}.value to be within min and max`);
591
+ }
592
+ let step;
593
+ if (component.steps !== undefined) {
594
+ const steps = expectFiniteNumber(component.steps, `${componentPath}.steps`);
595
+ if (!Number.isInteger(steps) || steps < 1 || steps > JSON_MAX_VALUES) {
596
+ throw new A2uiParseError(`Expected ${componentPath}.steps to be an integer from 1 through ${JSON_MAX_VALUES}`);
597
+ }
598
+ step = (maximum - minimum) / steps;
599
+ if (!Number.isFinite(step) || step <= 0) {
600
+ throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} has invalid range`);
601
+ }
602
+ if (!isSliderStepValue(value, minimum, step)) {
603
+ throw new A2uiParseError(`A2UI Slider ${JSON.stringify(component.id)} value does not align with its steps`);
604
+ }
605
+ }
606
+ const props = {
607
+ value,
608
+ minimum,
609
+ maximum,
610
+ ...(step === undefined ? {} : { step }),
611
+ };
612
+ if (component.label !== undefined) {
613
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
614
+ }
615
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
616
+ addCommonProps(component, props, context, scope);
617
+ addValidationProps(component, props, context, scope, "input");
618
+ if (props.accessibilityLabel === undefined) {
619
+ if (props.label === undefined) {
620
+ throw new A2uiParseError(`A2UI native Slider ${JSON.stringify(component.id)} requires label or accessibility.label`);
621
+ }
622
+ props.accessibilityLabel = props.label;
623
+ }
624
+ return { key, component: "Slider", props };
625
+ }
626
+ function isSliderStepValue(value, minimum, step) {
627
+ const offset = (value - minimum) / step;
628
+ return (Math.abs(offset - Math.round(offset)) <= Number.EPSILON * Math.max(1, Math.abs(offset)) * 8);
629
+ }
630
+ function adaptDateTimeInput(component, key, context, scope) {
631
+ const componentPath = `components.${component.id}`;
632
+ const enableDate = component.enableDate === true;
633
+ const enableTime = component.enableTime === true;
634
+ if (!enableDate && !enableTime) {
635
+ throw new A2uiParseError(`A2UI DateTimeInput ${JSON.stringify(component.id)} must enable date, time, or both`);
636
+ }
637
+ const value = resolveDynamicString(component.value, `${componentPath}.value`, context, scope);
638
+ validateDateTimeInputValue(value, enableDate, enableTime, `${componentPath}.value`, true);
639
+ const minimum = component.min === undefined
640
+ ? undefined
641
+ : resolveDynamicString(component.min, `${componentPath}.min`, context, scope);
642
+ const maximum = component.max === undefined
643
+ ? undefined
644
+ : resolveDynamicString(component.max, `${componentPath}.max`, context, scope);
645
+ if (minimum !== undefined) {
646
+ validateDateTimeInputValue(minimum, enableDate, enableTime, `${componentPath}.min`, false);
647
+ }
648
+ if (maximum !== undefined) {
649
+ validateDateTimeInputValue(maximum, enableDate, enableTime, `${componentPath}.max`, false);
650
+ }
651
+ if (minimum !== undefined &&
652
+ maximum !== undefined &&
653
+ dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`) >
654
+ dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
655
+ throw new A2uiParseError(`Expected ${componentPath}.min not to exceed max`);
656
+ }
657
+ const valueSort = value.length === 0
658
+ ? undefined
659
+ : dateTimeInputSortValue(value, enableDate, enableTime, `${componentPath}.value`);
660
+ if (valueSort !== undefined &&
661
+ minimum !== undefined &&
662
+ valueSort < dateTimeInputSortValue(minimum, enableDate, enableTime, `${componentPath}.min`)) {
663
+ throw new A2uiParseError(`Expected ${componentPath}.value not to be earlier than min`);
664
+ }
665
+ if (valueSort !== undefined &&
666
+ maximum !== undefined &&
667
+ valueSort > dateTimeInputSortValue(maximum, enableDate, enableTime, `${componentPath}.max`)) {
668
+ throw new A2uiParseError(`Expected ${componentPath}.value not to be later than max`);
669
+ }
670
+ const props = {
671
+ value,
672
+ enableDate,
673
+ enableTime,
674
+ ...(minimum === undefined ? {} : { minimum }),
675
+ ...(maximum === undefined ? {} : { maximum }),
676
+ };
677
+ if (component.label !== undefined) {
678
+ props.label = resolveDynamicString(component.label, `${componentPath}.label`, context, scope);
679
+ }
680
+ addBindingProp(component.value, `${componentPath}.value`, props, scope);
681
+ addCommonProps(component, props, context, scope);
682
+ addValidationProps(component, props, context, scope, "input");
683
+ if (props.accessibilityLabel === undefined) {
684
+ if (props.label === undefined) {
685
+ throw new A2uiParseError(`A2UI native DateTimeInput ${JSON.stringify(component.id)} requires label or accessibility.label`);
686
+ }
687
+ props.accessibilityLabel = props.label;
688
+ }
689
+ return { key, component: "DateTimeInput", props };
690
+ }
691
+ function adaptTabs(component, key, context, scope) {
692
+ const componentPath = `components.${component.id}`;
693
+ if (!Array.isArray(component.tabs) || component.tabs.length === 0) {
694
+ throw new A2uiParseError(`Expected a non-empty array at ${componentPath}.tabs`);
695
+ }
696
+ const tabs = component.tabs.map((value, index) => {
697
+ const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
698
+ return Object.freeze({
699
+ title: resolveDynamicString(tab.title, `${componentPath}.tabs[${index}].title`, context, scope),
700
+ });
701
+ });
702
+ const props = { tabs: Object.freeze(tabs) };
703
+ addCommonProps(component, props, context, scope);
704
+ return {
705
+ key,
706
+ component: "Tabs",
707
+ props,
708
+ children: component.tabs.map((value, index) => {
709
+ const tab = expectObject(value, `${componentPath}.tabs[${index}]`);
710
+ const childId = expectString(tab.child, `${componentPath}.tabs[${index}].child`);
711
+ return adaptComponent(childId, appendInstanceKey(key, childId, index), context, scope);
712
+ }),
713
+ };
714
+ }
715
+ function adaptModal(component, key, context, scope) {
716
+ const componentPath = `components.${component.id}`;
717
+ const triggerId = expectString(component.trigger, `${componentPath}.trigger`);
718
+ const contentId = expectString(component.content, `${componentPath}.content`);
719
+ if (context.surface.components.get(triggerId)?.component !== "Button") {
720
+ throw new A2uiParseError(`A2UI native Modal ${JSON.stringify(component.id)} requires a Button trigger`);
721
+ }
722
+ const props = {};
723
+ addCommonProps(component, props, context, scope);
724
+ return {
725
+ key,
726
+ component: "Modal",
727
+ props,
728
+ children: [
729
+ adaptComponent(triggerId, appendInstanceKey(key, triggerId, 0), context, scope),
730
+ adaptComponent(contentId, appendInstanceKey(key, contentId, 1), context, scope),
731
+ ],
732
+ };
733
+ }
734
+ function addBindingProp(value, path, props, scope) {
735
+ if (value !== undefined && isBinding(value)) {
736
+ props.binding = resolveBindingPointer(value.path, `${path}.path`, scope);
737
+ }
738
+ }
739
+ function resolveDynamicStringList(value, path, context, scope) {
740
+ const resolved = resolveDynamicValue(value, path, context, scope);
741
+ if (!Array.isArray(resolved) || resolved.some((item) => typeof item !== "string")) {
742
+ throw new A2uiParseError(`Expected an array of strings at ${path}`);
743
+ }
744
+ return [...resolved];
745
+ }
367
746
  function addValidationProps(component, props, context, scope, target) {
368
747
  if (component.checks === undefined) {
369
748
  return;
@@ -608,15 +987,18 @@ function resolveButtonAction(component, key, context, scope) {
608
987
  };
609
988
  }
610
989
  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}`);
990
+ return normalizeHttpUrl(value, path, "openUrl", A2UI_V1_NATIVE_MAX_OPEN_URL_LENGTH);
991
+ }
992
+ function normalizeHttpUrl(value, path, label, maximumLength) {
993
+ if (value.length === 0 || value.length > maximumLength) {
994
+ throw new A2uiParseError(`Expected an HTTP(S) URL up to ${maximumLength} characters at ${path}`);
613
995
  }
614
996
  if (/\s|\p{Cf}/u.test(value) || hasAsciiControlCharacter(value)) {
615
997
  throw new A2uiParseError(`Expected an HTTP(S) URL without whitespace, control, or Unicode format characters at ${path}`);
616
998
  }
617
999
  const UrlConstructor = globalThis.URL;
618
1000
  if (UrlConstructor === undefined) {
619
- throw new A2uiParseError(`The host runtime cannot validate an openUrl value at ${path}`);
1001
+ throw new A2uiParseError(`The host runtime cannot validate an ${label} value at ${path}`);
620
1002
  }
621
1003
  let parsed;
622
1004
  try {
@@ -629,13 +1011,105 @@ function normalizeOpenUrl(value, path) {
629
1011
  throw new A2uiParseError(`Expected an HTTP(S) URL at ${path}`);
630
1012
  }
631
1013
  if (parsed.username.length > 0 || parsed.password.length > 0) {
632
- throw new A2uiParseError(`A2UI openUrl does not allow URL credentials at ${path}`);
1014
+ throw new A2uiParseError(`A2UI ${label} does not allow URL credentials at ${path}`);
633
1015
  }
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}`);
1016
+ if (parsed.href.length > maximumLength) {
1017
+ throw new A2uiParseError(`Canonical A2UI ${label} at ${path} exceeds maximum length of ${maximumLength}`);
636
1018
  }
637
1019
  return parsed.href;
638
1020
  }
1021
+ function parseImageResourcePolicy(value, path, context) {
1022
+ const policy = parseJsonObject(value, path);
1023
+ const allowedKeys = new Set([
1024
+ "allowedRedirectOrigins",
1025
+ "cacheMode",
1026
+ "maximumBytes",
1027
+ "maximumDecodedHeight",
1028
+ "maximumDecodedPixels",
1029
+ "maximumDecodedWidth",
1030
+ "maximumRedirects",
1031
+ ]);
1032
+ for (const key of Object.keys(policy)) {
1033
+ if (!allowedKeys.has(key)) {
1034
+ throw new A2uiParseError(`Unexpected field ${JSON.stringify(key)} at ${path}`);
1035
+ }
1036
+ }
1037
+ if (!Array.isArray(policy.allowedRedirectOrigins) ||
1038
+ policy.allowedRedirectOrigins.length > A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS) {
1039
+ throw new A2uiParseError(`Expected at most ${A2UI_V1_NATIVE_MAX_IMAGE_REDIRECT_ORIGINS} redirect origins at ${path}.allowedRedirectOrigins`);
1040
+ }
1041
+ context.imageRedirectOriginCount += policy.allowedRedirectOrigins.length;
1042
+ if (context.imageRedirectOriginCount > JSON_MAX_VALUES) {
1043
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum of ${JSON_MAX_VALUES} image redirect origins`);
1044
+ }
1045
+ const origins = policy.allowedRedirectOrigins.map((origin, index) => parseExactHttpOrigin(origin, `${path}.allowedRedirectOrigins[${index}]`));
1046
+ if (new Set(origins).size !== origins.length) {
1047
+ throw new A2uiParseError(`Expected unique redirect origins at ${path}.allowedRedirectOrigins`);
1048
+ }
1049
+ context.imageResourcePolicyOutputLength += origins.reduce((length, origin) => length + origin.length, 0);
1050
+ if (context.imageResourcePolicyOutputLength > A2UI_V1_MAX_SOURCE_LENGTH) {
1051
+ throw new A2uiParseError(`Expanded A2UI native plan exceeds maximum image-policy output length of ${A2UI_V1_MAX_SOURCE_LENGTH}`);
1052
+ }
1053
+ const cacheMode = policy.cacheMode;
1054
+ if (cacheMode !== "default" && cacheMode !== "no-store") {
1055
+ throw new A2uiParseError(`Expected a closed cache mode at ${path}.cacheMode`);
1056
+ }
1057
+ const maximumBytes = parseBoundedPositiveInteger(policy.maximumBytes, A2UI_V1_NATIVE_MAX_IMAGE_BYTES, `${path}.maximumBytes`);
1058
+ const maximumDecodedWidth = parseBoundedPositiveInteger(policy.maximumDecodedWidth, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedWidth`);
1059
+ const maximumDecodedHeight = parseBoundedPositiveInteger(policy.maximumDecodedHeight, A2UI_V1_NATIVE_MAX_IMAGE_DIMENSION, `${path}.maximumDecodedHeight`);
1060
+ const maximumDecodedPixels = parseBoundedPositiveInteger(policy.maximumDecodedPixels, A2UI_V1_NATIVE_MAX_IMAGE_PIXELS, `${path}.maximumDecodedPixels`);
1061
+ if (maximumDecodedPixels > maximumDecodedWidth * maximumDecodedHeight) {
1062
+ throw new A2uiParseError(`Expected ${path}.maximumDecodedPixels not to exceed the declared dimensions`);
1063
+ }
1064
+ const maximumRedirects = parseBoundedNonNegativeInteger(policy.maximumRedirects, A2UI_V1_NATIVE_MAX_IMAGE_REDIRECTS, `${path}.maximumRedirects`);
1065
+ return Object.freeze({
1066
+ allowedRedirectOrigins: Object.freeze(origins),
1067
+ cacheMode,
1068
+ maximumBytes,
1069
+ maximumDecodedHeight,
1070
+ maximumDecodedPixels,
1071
+ maximumDecodedWidth,
1072
+ maximumRedirects,
1073
+ });
1074
+ }
1075
+ function parseExactHttpOrigin(value, path) {
1076
+ const origin = expectString(value, path);
1077
+ if (/\s|\p{Cf}/u.test(origin) || hasAsciiControlCharacter(origin)) {
1078
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
1079
+ }
1080
+ const UrlConstructor = globalThis.URL;
1081
+ if (UrlConstructor === undefined) {
1082
+ throw new A2uiParseError(`The host runtime cannot validate an image origin at ${path}`);
1083
+ }
1084
+ let parsed;
1085
+ try {
1086
+ parsed = new UrlConstructor(origin);
1087
+ }
1088
+ catch (cause) {
1089
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`, { cause });
1090
+ }
1091
+ if ((parsed.protocol !== "https:" && parsed.protocol !== "http:") ||
1092
+ parsed.username.length > 0 ||
1093
+ parsed.password.length > 0 ||
1094
+ parsed.origin !== origin) {
1095
+ throw new A2uiParseError(`Expected an exact HTTP(S) origin at ${path}`);
1096
+ }
1097
+ return parsed.origin;
1098
+ }
1099
+ function parseBoundedPositiveInteger(value, maximum, path) {
1100
+ const result = expectFiniteNumber(value, path);
1101
+ if (!Number.isInteger(result) || result < 1 || result > maximum) {
1102
+ throw new A2uiParseError(`Expected an integer from 1 through ${maximum} at ${path}`);
1103
+ }
1104
+ return result;
1105
+ }
1106
+ function parseBoundedNonNegativeInteger(value, maximum, path) {
1107
+ const result = expectFiniteNumber(value, path);
1108
+ if (!Number.isInteger(result) || result < 0 || result > maximum) {
1109
+ throw new A2uiParseError(`Expected an integer from 0 through ${maximum} at ${path}`);
1110
+ }
1111
+ return result;
1112
+ }
639
1113
  function hasAsciiControlCharacter(value) {
640
1114
  for (let index = 0; index < value.length; index += 1) {
641
1115
  const code = value.charCodeAt(index);
@@ -803,6 +1277,7 @@ function resolveNumberFormat(call, path, context, scope) {
803
1277
  }
804
1278
  const DATE_NUMBER = /^[+-]?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/;
805
1279
  const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
1280
+ const TIME_ONLY = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,9}))?)?(Z|([+-])(\d{2}):(\d{2}))?$/;
806
1281
  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
1282
  const A2UI_V1_MAX_DATE_PATTERN_TOKENS = 128;
808
1283
  const DATE_PATTERN_TOKENS = Object.freeze([
@@ -825,6 +1300,98 @@ const DATE_PATTERN_TOKENS = Object.freeze([
825
1300
  "a",
826
1301
  ]);
827
1302
  const DATE_PATTERN_TOKEN_SET = new Set(DATE_PATTERN_TOKENS);
1303
+ function validateDateTimeInputValue(value, enableDate, enableTime, path, allowEmpty) {
1304
+ if (allowEmpty && value.length === 0) {
1305
+ return;
1306
+ }
1307
+ if (enableDate && enableTime) {
1308
+ parseDateValue(value, path);
1309
+ if (RFC_3339_TIMESTAMP.exec(value) === null) {
1310
+ throw new A2uiParseError(`Expected an RFC 3339 date-time at ${path}`);
1311
+ }
1312
+ return;
1313
+ }
1314
+ if (enableDate) {
1315
+ const match = DATE_ONLY.exec(value);
1316
+ if (match === null) {
1317
+ throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
1318
+ }
1319
+ validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
1320
+ return;
1321
+ }
1322
+ parseTimeOnly(value, path);
1323
+ }
1324
+ /** Internal mounted-renderer guard for values emitted by a trusted host date/time component. */
1325
+ export function validateA2uiV1NativeDateTimeInputChange(value, enableDate, enableTime, minimum, maximum, path) {
1326
+ if (!enableDate && !enableTime) {
1327
+ throw new A2uiParseError(`A2UI DateTimeInput at ${path} must enable date, time, or both`);
1328
+ }
1329
+ validateDateTimeInputValue(value, enableDate, enableTime, `${path}.value`, true);
1330
+ if (minimum !== undefined) {
1331
+ validateDateTimeInputValue(minimum, enableDate, enableTime, `${path}.minimum`, false);
1332
+ }
1333
+ if (maximum !== undefined) {
1334
+ validateDateTimeInputValue(maximum, enableDate, enableTime, `${path}.maximum`, false);
1335
+ }
1336
+ const minimumSort = minimum === undefined
1337
+ ? undefined
1338
+ : dateTimeInputSortValue(minimum, enableDate, enableTime, `${path}.minimum`);
1339
+ const maximumSort = maximum === undefined
1340
+ ? undefined
1341
+ : dateTimeInputSortValue(maximum, enableDate, enableTime, `${path}.maximum`);
1342
+ if (minimumSort !== undefined && maximumSort !== undefined && minimumSort > maximumSort) {
1343
+ throw new A2uiParseError(`Expected ${path}.minimum not to exceed maximum`);
1344
+ }
1345
+ if (value.length === 0) {
1346
+ return;
1347
+ }
1348
+ const valueSort = dateTimeInputSortValue(value, enableDate, enableTime, `${path}.value`);
1349
+ if (minimumSort !== undefined && valueSort < minimumSort) {
1350
+ throw new A2uiParseError(`Expected ${path}.value not to be earlier than minimum`);
1351
+ }
1352
+ if (maximumSort !== undefined && valueSort > maximumSort) {
1353
+ throw new A2uiParseError(`Expected ${path}.value not to be later than maximum`);
1354
+ }
1355
+ }
1356
+ function dateTimeInputSortValue(value, enableDate, enableTime, path) {
1357
+ if (enableDate && enableTime) {
1358
+ return parseDateValue(value, path).getTime();
1359
+ }
1360
+ if (enableDate) {
1361
+ const match = DATE_ONLY.exec(value);
1362
+ if (match === null) {
1363
+ throw new A2uiParseError(`Expected a yyyy-MM-dd date at ${path}`);
1364
+ }
1365
+ validateCalendarDate(Number(match[1]), Number(match[2]), Number(match[3]), path);
1366
+ return Date.UTC(Number(match[1]), Number(match[2]) - 1, Number(match[3]));
1367
+ }
1368
+ return parseTimeOnly(value, path);
1369
+ }
1370
+ function parseTimeOnly(value, path) {
1371
+ const match = TIME_ONLY.exec(value);
1372
+ if (match === null) {
1373
+ throw new A2uiParseError(`Expected an ISO 8601 time at ${path}`);
1374
+ }
1375
+ const hour = Number(match[1]);
1376
+ const minute = Number(match[2]);
1377
+ const second = Number(match[3] ?? "0");
1378
+ if (hour > 23 || minute > 59 || second > 59) {
1379
+ throw new A2uiParseError(`Invalid ISO 8601 time at ${path}`);
1380
+ }
1381
+ const fraction = match[4] ?? "";
1382
+ const millisecond = Number(fraction.slice(0, 3).padEnd(3, "0"));
1383
+ let result = ((hour * 60 + minute) * 60 + second) * 1_000 + millisecond;
1384
+ if (match[5] !== undefined && match[5] !== "Z") {
1385
+ const offsetHour = Number(match[7]);
1386
+ const offsetMinute = Number(match[8]);
1387
+ if (offsetHour > 23 || offsetMinute > 59) {
1388
+ throw new A2uiParseError(`Invalid ISO 8601 time offset at ${path}`);
1389
+ }
1390
+ const direction = match[6] === "+" ? 1 : -1;
1391
+ result -= direction * (offsetHour * 60 + offsetMinute) * 60_000;
1392
+ }
1393
+ return result;
1394
+ }
828
1395
  function resolveDateFormat(call, path, context, scope) {
829
1396
  const args = expectObject(call.args, `${path}.args`);
830
1397
  const value = resolveDynamicValue(args.value, `${path}.args.value`, context, scope);