@jini-ai/ui 0.6.22 → 0.6.23

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/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## 0.6.23
2
+
3
+ - Security hardening.
4
+
1
5
  # @jini-ai/ui
2
6
 
3
7
  ## 0.6.22
@@ -15,7 +15,7 @@ import { buildOutcomeSurface } from './surfaces/outcome.js';
15
15
  /** Reject ambiguous or protocol-reserved names and runtime attempts to prefill secrets.
16
16
  * @complexity O(n) time/space in the field count. Throws fixed ToolInputError text only. */
17
17
  function checkedFields(form) {
18
- const names = new Set([SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWER_PARAM]);
18
+ const names = new Set([SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWER_PARAM, 'freeText']);
19
19
  for (const field of form.fields) {
20
20
  if (!field.name || names.has(field.name))
21
21
  throw new ToolInputError({ message: 'Secret card field names must be unique and cannot use exchange parameters.' });
@@ -36,6 +36,9 @@ function answerStep(required) {
36
36
  return { run: { status: answer.status } };
37
37
  if (answer.params[SURFACE_DISMISSED_PARAM] === true)
38
38
  return { run: { status: 'cancelled' } };
39
+ // Composer answers are displayed as chat text, never accepted as secret form submissions.
40
+ if (Object.hasOwn(answer.params, SURFACE_TYPED_ANSWER_PARAM))
41
+ return { run: { status: 'cancelled' } };
39
42
  return { params: answer.params };
40
43
  }
41
44
  /** The exact submitted secret, or undefined when it is blank or malformed.
@@ -137,7 +137,7 @@ function confirmationBodyHtml(spec, required) {
137
137
  renderChoices(spec.choices ?? []),
138
138
  warning,
139
139
  // Confirm starts disabled; the script enables it once the dwell has run out.
140
- renderActions(confirmationActions(spec).map((action) => (affirmative.has(action.id) ? { ...action, disabled: true } : action))),
140
+ renderActions(confirmationActions(spec).map((action) => (affirmative.has(action.id) ? { ...action, disabled: true } : action)), { humanOnly: true }),
141
141
  renderStatusRegion(),
142
142
  ]
143
143
  .filter((fragment) => fragment !== '')
@@ -179,16 +179,26 @@ export interface SurfaceAction {
179
179
  * `script-src 'unsafe-inline'`-equivalent allowance for attributes specifically, and a host is free
180
180
  * to apply a stricter CSP to the frame than this package can see.
181
181
  *
182
- * Each button also carries the `@jini-ai/agentic` `data-agent-*` markup convention
182
+ * Ordinary buttons also carry the `@jini-ai/agentic` `data-agent-*` markup convention
183
183
  * ({@link AGENT_ELEMENT_ATTRIBUTE} et al.) — but this tagging is advisory, not the thing that makes
184
184
  * a surface's actions reachable by `page.find_elements`/`page.click`. Those capabilities resolve a
185
185
  * handle by scanning `document`/`contentDocument`, and this document renders inside a `srcdoc`
186
186
  * iframe sandboxed to `allow-scripts` alone (`MCP_UI_VIEW_SANDBOX` — no `allow-same-origin`,
187
187
  * deliberately), which gives it an opaque origin no ancestor document can read into. So this tagging
188
188
  * is real for a driver that reaches INTO the frame directly — `frameLocator` in Playwright, proven
189
- * end to end against exactly this markup — and inert for anything scanning the parent page. Making a
190
- * surface's actions visible to `page.find_elements` needs a second, parent-DOM-side echo of this
191
- * same action list; see `McpUiSurfaceCard` in `@jini-ai/chat` for that half.
189
+ * end to end against exactly this markup — and inert for anything scanning the parent page. Making an
190
+ * ordinary surface's actions visible to `page.find_elements` needs a second, parent-DOM-side echo
191
+ * of that action list. Human-only decisions are intentionally excluded from addressing even
192
+ * inside the frame: confirmation buttons are private and their runtime still requires trusted clicks.
193
+ *
194
+ * @param actions - Action descriptions; the legacy array argument is retained for caller compatibility.
195
+ * @param options - Human-only rows omit agent addressing and mark every decision private.
196
+ * @returns Escaped HTML without executing actions or changing their runtime click gates.
197
+ * @complexity O(n) time and space in the emitted markup length.
198
+ * @overallScore 98/100
199
+ * @qualityFindings Low: The legacy array-first signature remains a compatibility exception to object arguments.
192
200
  */
193
- export declare function renderActions(actions: readonly SurfaceAction[]): string;
201
+ export declare function renderActions(actions: readonly SurfaceAction[], { humanOnly }?: {
202
+ humanOnly?: boolean;
203
+ }): string;
194
204
  //# sourceMappingURL=document.d.ts.map
@@ -11,7 +11,7 @@
11
11
  * builders because the CSS that styles them lives here — splitting the markup from the only
12
12
  * stylesheet that can reach it is how a "generic" fragment quietly becomes unstyled in one caller.
13
13
  */
14
- import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE } from '@jini-ai/agentic';
14
+ import { AGENT_ELEMENT_ATTRIBUTE, AGENT_LABEL_ATTRIBUTE, AGENT_PRIVATE_ATTRIBUTE, AGENT_ROLE_ATTRIBUTE } from '@jini-ai/agentic';
15
15
  import { escapeHtml } from '../escape.js';
16
16
  import { SURFACE_BRIDGE_GLOBAL, renderBridgeScript } from './bridge.js';
17
17
  import { renderTokenBlock } from './tokens.js';
@@ -429,18 +429,26 @@ export function fieldDescribedBy(spec) {
429
429
  * `script-src 'unsafe-inline'`-equivalent allowance for attributes specifically, and a host is free
430
430
  * to apply a stricter CSP to the frame than this package can see.
431
431
  *
432
- * Each button also carries the `@jini-ai/agentic` `data-agent-*` markup convention
432
+ * Ordinary buttons also carry the `@jini-ai/agentic` `data-agent-*` markup convention
433
433
  * ({@link AGENT_ELEMENT_ATTRIBUTE} et al.) — but this tagging is advisory, not the thing that makes
434
434
  * a surface's actions reachable by `page.find_elements`/`page.click`. Those capabilities resolve a
435
435
  * handle by scanning `document`/`contentDocument`, and this document renders inside a `srcdoc`
436
436
  * iframe sandboxed to `allow-scripts` alone (`MCP_UI_VIEW_SANDBOX` — no `allow-same-origin`,
437
437
  * deliberately), which gives it an opaque origin no ancestor document can read into. So this tagging
438
438
  * is real for a driver that reaches INTO the frame directly — `frameLocator` in Playwright, proven
439
- * end to end against exactly this markup — and inert for anything scanning the parent page. Making a
440
- * surface's actions visible to `page.find_elements` needs a second, parent-DOM-side echo of this
441
- * same action list; see `McpUiSurfaceCard` in `@jini-ai/chat` for that half.
439
+ * end to end against exactly this markup — and inert for anything scanning the parent page. Making an
440
+ * ordinary surface's actions visible to `page.find_elements` needs a second, parent-DOM-side echo
441
+ * of that action list. Human-only decisions are intentionally excluded from addressing even
442
+ * inside the frame: confirmation buttons are private and their runtime still requires trusted clicks.
443
+ *
444
+ * @param actions - Action descriptions; the legacy array argument is retained for caller compatibility.
445
+ * @param options - Human-only rows omit agent addressing and mark every decision private.
446
+ * @returns Escaped HTML without executing actions or changing their runtime click gates.
447
+ * @complexity O(n) time and space in the emitted markup length.
448
+ * @overallScore 98/100
449
+ * @qualityFindings Low: The legacy array-first signature remains a compatibility exception to object arguments.
442
450
  */
443
- export function renderActions(actions) {
451
+ export function renderActions(actions, { humanOnly = false } = {}) {
444
452
  const buttons = actions
445
453
  .map((action) => {
446
454
  const variant = action.variant ?? 'neutral';
@@ -454,7 +462,9 @@ export function renderActions(actions) {
454
462
  // would accept. That is unenforced on purpose (see the function doc above: `page.find_elements`
455
463
  // can never reach this document to resolve it either way), so failing loudly here would refuse
456
464
  // markup for a reason that can never matter to the one consumer that can actually query it.
457
- return ` <button type="${action.type ?? 'button'}" class="mcpui-button${variantClass}" data-mcpui-action="${escapeHtml(action.id)}" ${AGENT_ELEMENT_ATTRIBUTE}="mcpui-action-${escapeHtml(action.id)}" ${AGENT_ROLE_ATTRIBUTE}="button" ${AGENT_LABEL_ATTRIBUTE}="${label}"${action.disabled === true ? ' disabled' : ''}>${label}</button>`;
465
+ const addressing = humanOnly ? AGENT_PRIVATE_ATTRIBUTE
466
+ : `${AGENT_ELEMENT_ATTRIBUTE}="mcpui-action-${escapeHtml(action.id)}" ${AGENT_ROLE_ATTRIBUTE}="button" ${AGENT_LABEL_ATTRIBUTE}="${label}"`;
467
+ return ` <button type="${action.type ?? 'button'}" class="mcpui-button${variantClass}" data-mcpui-action="${escapeHtml(action.id)}" ${addressing}${action.disabled === true ? ' disabled' : ''}>${label}</button>`;
458
468
  })
459
469
  .join('\n');
460
470
  return `<div class="mcpui-actions">\n${buttons}\n</div>`;
@@ -1,7 +1,8 @@
1
1
  import type { InputHTMLAttributes, TextareaHTMLAttributes } from 'react';
2
2
  /**
3
3
  * @file The shared masked input: a password/token/key `<input>` with an eye toggle inside its right
4
- * edge. Drop-in for `<input type="password">` — every other input prop passes straight through.
4
+ * edge. Drop-in for `<input type="password">` — ordinary input props pass through; agent
5
+ * addressing is excluded and privacy remains enforced after reveal.
5
6
  *
6
7
  * Same markup and classes as `@jini-ai/ui-kit`'s password `TextField` and the mcp-ui secret fields
7
8
  * (`.jini-secret` / `.jini-secret-toggle`), so one host stylesheet styles all of them; hosts own
@@ -25,7 +26,7 @@ interface SecretToggleProps {
25
26
  readonly show: string;
26
27
  readonly hide: string;
27
28
  };
28
- /** Extra attributes for the toggle button, e.g. an agent handle. */
29
+ /** Extra ordinary attributes for the toggle button; agent addressing is always excluded. */
29
30
  readonly toggleAttrs?: Readonly<Record<string, string | boolean | undefined>>;
30
31
  /** Controlled mode: the host owns the shown/hidden state. */
31
32
  readonly revealed?: boolean;
@@ -1,16 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { forwardRef } from 'react';
3
3
  import { SecretInput as KitSecretInput } from '@jini-ai/ui-kit/react';
4
+ import { secretControlAttrs } from '@jini-ai/ui-kit';
4
5
  import { useT } from '../../features/i18n/index.js';
5
6
  import { Icon } from './Icon.js';
6
7
  import { useSecretReveal } from './SecretInput.hooks.js';
7
- /** Reveal state + the eye button, shared by `SecretInput` and `SecretTextarea`. */
8
+ /** Reveal state + the private eye button, shared by `SecretInput` and `SecretTextarea`.
9
+ * @complexity O(p) time and space in toggle props; no I/O.
10
+ * @overallScore 100 */
8
11
  function useSecretToggle({ label, toggleLabels, toggleAttrs, revealed, onRevealedChange }, { value, disabled } = {}) {
9
12
  const t = useT();
10
13
  const reveal = useSecretReveal({ value, labels: toggleLabels ?? { show: `${t('Show')} ${label}`, hide: `${t('Hide')} ${label}` } }, { ...(revealed === undefined ? {} : { revealed }), ...(onRevealedChange ? { onRevealedChange } : {}) });
11
14
  // One signal only: the name flips Show/Hide, so no `aria-pressed` — both together made screen
12
15
  // readers announce "Hide API key, toggle button, pressed" (Opus UI review 2026-10-26).
13
- const button = (_jsx("button", { ...toggleAttrs, type: "button", className: "jini-secret-toggle", "aria-label": reveal.toggleLabel, title: reveal.toggleLabel, disabled: disabled, onClick: reveal.toggle, children: _jsx(Icon, { name: reveal.revealed ? 'eye-off' : 'eye', size: 18 }) }));
16
+ const button = (_jsx("button", { ...secretControlAttrs({ attrs: toggleAttrs ?? {} }), type: "button", className: "jini-secret-toggle", "aria-label": reveal.toggleLabel, title: reveal.toggleLabel, disabled: disabled, onClick: reveal.toggle, children: _jsx(Icon, { name: reveal.revealed ? 'eye-off' : 'eye', size: 18 }) }));
14
17
  return { reveal, button };
15
18
  }
16
19
  export const SecretInput = forwardRef(function SecretInput(props, ref) {
@@ -26,6 +29,6 @@ export const SecretTextarea = forwardRef(function SecretTextarea(props, ref) {
26
29
  /* Not a standard CSS property, so it is set here rather than left to a host stylesheet: a host
27
30
  that forgot it would show the secret in clear text. pre-wrap keeps line breaks while masked. */
28
31
  const masked = { ...style, ...{ WebkitTextSecurity: reveal.revealed || replacementShown ? 'none' : 'disc' }, whiteSpace: 'pre-wrap' };
29
- return (_jsxs("span", { className: "jini-secret jini-secret--multiline", children: [_jsx("textarea", { ...textareaProps, ...(replacementShown ? { value: maskedValue, readOnly: true } : {}), spellCheck: false, ref: ref, style: masked }), button] }));
32
+ return (_jsxs("span", { className: "jini-secret jini-secret--multiline", children: [_jsx("textarea", { ...secretControlAttrs({ attrs: textareaProps }), ...(replacementShown ? { value: maskedValue, readOnly: true } : {}), spellCheck: false, ref: ref, style: masked }), button] }));
30
33
  });
31
34
  //# sourceMappingURL=SecretInput.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jini-ai/ui",
3
- "version": "0.6.22",
3
+ "version": "0.6.23",
4
4
  "description": "Generic, product-neutral React UI primitives and feature domains. Framework-free types, decision rules, catalogs, and host port contracts live behind ./core (no React, no DOM). The Excalidraw and Lexical editors live behind ./sketch-editor and ./lexical-rich-text-editor; the GrapesJS-backed inline HTML editor lives behind ./html-editor.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -263,19 +263,19 @@
263
263
  "shiki": "^1.24.4",
264
264
  "tailwind-merge": "^2.6.0",
265
265
  "zod": "^3.25.76",
266
- "@jini-ai/platform": "^0.6.22",
267
- "@jini-ai/agent-runtime": "^0.6.22",
266
+ "@jini-ai/agent-runtime": "^0.6.23",
267
+ "@jini-ai/agentic": "^0.6.23",
268
268
  "@jini-ai/integrations": "^0.6.19",
269
- "@jini-ai/agentic": "^0.6.19",
270
- "@jini-ai/core": "^0.6.22"
269
+ "@jini-ai/core": "^0.6.23",
270
+ "@jini-ai/platform": "^0.6.23"
271
271
  },
272
272
  "dependencies": {
273
273
  "micromark": "^4.0.0",
274
274
  "micromark-extension-gfm": "^3.0.0",
275
- "@jini-ai/core": "^0.6.22",
276
- "@jini-ai/ui-kit": "^0.6.19",
277
- "@jini-ai/agentic": "^0.6.19",
278
- "@jini-ai/platform": "^0.6.22"
275
+ "@jini-ai/ui-kit": "^0.6.23",
276
+ "@jini-ai/core": "^0.6.23",
277
+ "@jini-ai/platform": "^0.6.23",
278
+ "@jini-ai/agentic": "^0.6.23"
279
279
  },
280
280
  "peerDependencies": {
281
281
  "@tanstack/react-query": "^5.0.0",