@jini-ai/ui 0.4.3 → 0.4.5
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 +11 -0
- package/README.md +20 -0
- package/dist/core/theme/default.d.ts +4 -0
- package/dist/core/theme/default.js +11 -0
- package/dist/core/theme/stylesheet.d.ts +8 -0
- package/dist/core/theme/stylesheet.js +14 -0
- package/dist/core/theme/types.d.ts +39 -0
- package/dist/core/theme/types.js +2 -0
- package/dist/core/theme/validation.d.ts +15 -0
- package/dist/core/theme/validation.js +55 -0
- package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
- package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
- package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
- package/dist/features/admin-widgets/components/Select/Select.js +4 -3
- package/dist/features/appearance/constants.js +1 -1
- package/dist/features/connectors/rules.d.ts +1 -6
- package/dist/features/connectors/types.d.ts +1 -6
- package/dist/features/connectors/types.js +1 -6
- package/dist/features/execution/http-adapter.d.ts +111 -0
- package/dist/features/execution/http-adapter.js +160 -0
- package/dist/features/execution/index.d.ts +4 -0
- package/dist/features/execution/index.js +2 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
- package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
- package/dist/features/html-editor/canvas-style.d.ts +1 -1
- package/dist/features/html-editor/canvas-style.js +2 -2
- package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
- package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
- package/dist/features/i18n/dictionary-translator.d.ts +5 -2
- package/dist/features/i18n/dictionary-translator.js +1 -1
- package/dist/features/i18n/index.d.ts +1 -1
- package/dist/features/i18n/index.js +1 -1
- package/dist/features/i18n/template-i18n.d.ts +11 -0
- package/dist/features/i18n/template-i18n.js +12 -0
- package/dist/features/interactive-ui/interactive-ui.css +1 -1
- package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
- package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
- package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
- package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
- package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
- package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
- package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
- package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
- package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
- package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
- package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
- package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
- package/dist/features/lexical-rich-text-editor/types.js +3 -13
- package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
- package/dist/features/mcp-ui/early-message-buffer.js +1 -2
- package/dist/features/mcp-ui/escape.d.ts +4 -0
- package/dist/features/mcp-ui/escape.js +2 -9
- package/dist/features/mcp-ui/index.d.ts +1 -0
- package/dist/features/mcp-ui/index.js +1 -0
- package/dist/features/mcp-ui/protocol.d.ts +2 -5
- package/dist/features/mcp-ui/protocol.js +2 -5
- package/dist/features/mcp-ui/resource.d.ts +47 -7
- package/dist/features/mcp-ui/resource.js +5 -8
- package/dist/features/mcp-ui/secret-card.d.ts +115 -0
- package/dist/features/mcp-ui/secret-card.js +137 -0
- package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
- package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
- package/dist/features/mcp-ui/surfaces/fields.js +3 -1
- package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
- package/dist/features/mcp-ui/surfaces/form.js +65 -3
- package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
- package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
- package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
- package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
- package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
- package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
- package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
- package/dist/features/panel-kit/helpers/permissions.js +2 -0
- package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
- package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
- package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
- package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
- package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
- package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
- package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
- package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
- package/dist/features/panel-kit/index.d.ts +7 -1
- package/dist/features/panel-kit/index.js +5 -1
- package/dist/features/resource-dashboard/types.d.ts +8 -41
- package/dist/features/resource-dashboard/types.js +3 -26
- package/dist/features/tab-strip/constants.d.ts +3 -4
- package/dist/features/tab-strip/constants.js +3 -4
- package/dist/features/tab-strip/dependencies.d.ts +1 -6
- package/dist/features/tab-strip/dependencies.js +1 -6
- package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
- package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
- package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
- package/dist/features/tab-strip/react/components/TabBar.js +12 -1
- package/dist/features/tab-strip/rules.d.ts +4 -17
- package/dist/features/tab-strip/rules.js +4 -17
- package/dist/features/tab-strip/types.d.ts +2 -13
- package/dist/icon-name.d.ts +1 -1
- package/dist/icon-name.js +1 -0
- package/dist/index.d.ts +42 -417
- package/dist/index.js +42 -430
- package/dist/react/components/CustomSelect.d.ts +6 -2
- package/dist/react/components/CustomSelect.js +20 -91
- package/dist/react/components/Icon.d.ts +6 -0
- package/dist/react/components/Icon.js +12 -0
- package/dist/react/components/select-popover.d.ts +106 -0
- package/dist/react/components/select-popover.js +210 -0
- package/dist/renderers/sandbox-scripts.d.ts +29 -0
- package/dist/renderers/sandbox-scripts.js +127 -0
- package/dist/renderers/sandboxed-document.js +3 -86
- package/dist/renderers/srcdoc/build.d.ts +10 -17
- package/dist/renderers/srcdoc/build.js +31 -234
- package/dist/styles/variables.css +30 -0
- package/dist/theme/apply.d.ts +1 -1
- package/dist/theme/apply.js +2 -2
- package/dist/theme/color-scheme.d.ts +1 -1
- package/dist/theme/default.d.ts +1 -3
- package/dist/theme/default.js +2 -10
- package/dist/theme/index.d.ts +3 -3
- package/dist/theme/index.js +2 -2
- package/dist/theme/stylesheet.d.ts +1 -7
- package/dist/theme/stylesheet.js +2 -13
- package/dist/theme/types.d.ts +1 -38
- package/dist/theme/types.js +2 -1
- package/dist/theme/validation.d.ts +1 -14
- package/dist/theme/validation.js +2 -54
- package/dist/utils/endpoint-policy.d.ts +21 -39
- package/dist/utils/endpoint-policy.js +19 -107
- package/dist/utils/file-system-errors.d.ts +1 -2
- package/dist/utils/file-system-errors.js +1 -2
- package/dist/utils/ime-composing.d.ts +0 -3
- package/dist/utils/ime-composing.js +0 -3
- package/dist/utils/platform.d.ts +1 -4
- package/dist/utils/platform.js +1 -4
- package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
- package/dist/utils/smooth-scroll-to-top.js +3 -13
- package/dist/utils/timezone.js +1 -5
- package/dist/utils/uuid.d.ts +0 -3
- package/dist/utils/uuid.js +0 -3
- package/dist/utils/visual-stability.d.ts +1 -3
- package/dist/utils/visual-stability.js +1 -3
- package/package.json +74 -15
|
@@ -12,11 +12,8 @@
|
|
|
12
12
|
* ``ui://${string}`` URI scheme and the wider MIME union reproduced in
|
|
13
13
|
* {@link UIResourceMimeType}.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
* JSON-RPC
|
|
17
|
-
* because its tsconfig's
|
|
18
|
-
* `moduleResolution: "Node"` cannot read a modern package's `exports` map, and this package has no
|
|
19
|
-
* such constraint — the envelope comes from `protocol.ts`'s re-exports instead. See
|
|
15
|
+
* The envelope comes from protocol.ts's re-exports, so this resource layer has no parallel
|
|
16
|
+
* JSON-RPC or UIActionResult definition. See
|
|
20
17
|
* `surfaces/bridge.ts` for why the legacy `{type:'tool', payload}` action shape is not emitted.
|
|
21
18
|
*/
|
|
22
19
|
/**
|
|
@@ -94,7 +91,7 @@ export const MCP_UI_EXPIRES_AT_META_KEY = `${JINI_MCP_UI_METADATA_PREFIX}expires
|
|
|
94
91
|
* @returns The `EmbeddedResource` to place in a tool result's `content` array.
|
|
95
92
|
* @complexity O(1).
|
|
96
93
|
*/
|
|
97
|
-
export function createUIResource(spec) {
|
|
94
|
+
export function createUIResource(spec, _optional = {}) {
|
|
98
95
|
const meta = {
|
|
99
96
|
...(spec.preferredFrameSize === undefined
|
|
100
97
|
? {}
|
|
@@ -109,7 +106,7 @@ export function createUIResource(spec) {
|
|
|
109
106
|
uri: spec.uri,
|
|
110
107
|
mimeType: MCP_UI_MIME_TYPE,
|
|
111
108
|
text: spec.htmlString,
|
|
112
|
-
...(Object.keys(meta).length === 0 ? {} : { _meta: meta }),
|
|
109
|
+
...(Object.keys(meta).length === 0 && spec.meta === undefined ? {} : { _meta: meta }),
|
|
113
110
|
},
|
|
114
111
|
};
|
|
115
112
|
}
|
|
@@ -122,7 +119,7 @@ export function createUIResource(spec) {
|
|
|
122
119
|
* @param spec.meta - Optional result-level `_meta`.
|
|
123
120
|
* @complexity O(1).
|
|
124
121
|
*/
|
|
125
|
-
export function buildUIToolResult(spec) {
|
|
122
|
+
export function buildUIToolResult(spec, _optional = {}) {
|
|
126
123
|
return {
|
|
127
124
|
content: [{ type: 'text', text: spec.modelText }, spec.ui],
|
|
128
125
|
...(spec.meta === undefined ? {} : { _meta: spec.meta }),
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One human-only secret-card lifecycle. Keystrokes go directly to the held exchange and save
|
|
3
|
+
* port, never through model input, chat text, or logs. Saved values must stay in the host's
|
|
4
|
+
* sealed store; save returns only a safe summary for result/outcome projections.
|
|
5
|
+
*
|
|
6
|
+
* Fail closed without an emitter: returning a form or accepting a second model call would
|
|
7
|
+
* strand the held call or route a secret through model context. Cancel posts a dismissal so
|
|
8
|
+
* the call wakes immediately instead of waiting for its TTL. askThenReport keeps the exchange
|
|
9
|
+
* open until the real save finishes; reusing the form URI replaces premature "Done." with its
|
|
10
|
+
* actual outcome. The host supplies the daemon's existing helper, never a second store.
|
|
11
|
+
*/
|
|
12
|
+
import { type SurfaceAskThenReport, type SurfaceExchangeStore, type ToolExecutionContext, type ToolExecutionOptions } from '@jini-ai/core';
|
|
13
|
+
import { type FormSurfaceSpec } from './surfaces/form.js';
|
|
14
|
+
import { type SurfaceOutcomeSpec } from './surfaces/outcome.js';
|
|
15
|
+
import type { StringField, SurfaceField } from './surfaces/fields.js';
|
|
16
|
+
/** Secret fields cannot carry a value, even through a previously declared variable.
|
|
17
|
+
* `allowBlank` permits an empty string for stored-value preservation or public-client auth. */
|
|
18
|
+
export type SecretCardField = (SurfaceField & {
|
|
19
|
+
readonly secret?: false;
|
|
20
|
+
}) | (Omit<StringField, 'value' | 'secret'> & {
|
|
21
|
+
readonly secret: true;
|
|
22
|
+
readonly value?: never;
|
|
23
|
+
readonly allowBlank?: boolean;
|
|
24
|
+
});
|
|
25
|
+
/** The host controls copy, field construction and display; routing/dismissal belong to the engine. */
|
|
26
|
+
export type SecretCardForm = Omit<FormSurfaceSpec, 'fields' | 'toolName' | 'baseParams' | 'cancel'> & {
|
|
27
|
+
readonly fields: readonly SecretCardField[];
|
|
28
|
+
readonly cancelLabel?: string;
|
|
29
|
+
};
|
|
30
|
+
export type SecretCardRun<Saved> = {
|
|
31
|
+
readonly status: 'saved';
|
|
32
|
+
readonly saved: Saved;
|
|
33
|
+
} | {
|
|
34
|
+
readonly status: 'cancelled';
|
|
35
|
+
} | {
|
|
36
|
+
readonly status: 'expired';
|
|
37
|
+
} | {
|
|
38
|
+
readonly status: 'abandoned';
|
|
39
|
+
} | {
|
|
40
|
+
readonly status: 'blank';
|
|
41
|
+
} | {
|
|
42
|
+
readonly status: 'failed';
|
|
43
|
+
readonly safeMessage: string;
|
|
44
|
+
};
|
|
45
|
+
/** A domain spec, with authorization and target resolution in prepare and persistence in save.
|
|
46
|
+
* Projections must contain safe metadata only. Neither projection receives the submitted values. */
|
|
47
|
+
export interface SecretCardSpec<Prep, Saved, Result> {
|
|
48
|
+
readonly toolId: string;
|
|
49
|
+
/** Validate public model input, authorize and resolve the target before opening a card.
|
|
50
|
+
* Reject secret-bearing input; never read stored plaintext for form construction. */
|
|
51
|
+
prepare(required: {
|
|
52
|
+
ctx: ToolExecutionContext;
|
|
53
|
+
}): Promise<Prep>;
|
|
54
|
+
form(required: {
|
|
55
|
+
prep: Prep;
|
|
56
|
+
}): SecretCardForm;
|
|
57
|
+
/** Only declared field names are forwarded. Domain validation remains authoritative.
|
|
58
|
+
* Values keep their native field types and exact bytes; stored secrets are never needed here. */
|
|
59
|
+
save(required: {
|
|
60
|
+
values: Readonly<Record<string, unknown>>;
|
|
61
|
+
prep: Prep;
|
|
62
|
+
signal: AbortSignal;
|
|
63
|
+
}): Promise<Saved>;
|
|
64
|
+
/** Return undefined for cancellation/expiry when the form already reports that state locally. */
|
|
65
|
+
outcome(required: {
|
|
66
|
+
prep: Prep;
|
|
67
|
+
run: SecretCardRun<Saved>;
|
|
68
|
+
}): SurfaceOutcomeSpec | undefined;
|
|
69
|
+
result(required: {
|
|
70
|
+
prep: Prep;
|
|
71
|
+
run: SecretCardRun<Saved>;
|
|
72
|
+
}): Result;
|
|
73
|
+
}
|
|
74
|
+
export interface SecretCardText {
|
|
75
|
+
readonly noEmitter?: string;
|
|
76
|
+
readonly saveFailure?: string;
|
|
77
|
+
readonly cancelLabel?: string;
|
|
78
|
+
}
|
|
79
|
+
export interface SecretCardOptions {
|
|
80
|
+
/** Allowlist known error kinds to fixed text. Never return raw error.message. */
|
|
81
|
+
readonly safeError?: (error: unknown) => string | undefined;
|
|
82
|
+
readonly text?: SecretCardText;
|
|
83
|
+
readonly frameSize?: readonly [string, string];
|
|
84
|
+
readonly uriHost?: string;
|
|
85
|
+
/** Fixed metadata only: errors and submitted values are deliberately absent. */
|
|
86
|
+
readonly logFailure?: (metadata: {
|
|
87
|
+
toolId: string;
|
|
88
|
+
exchangeId: string;
|
|
89
|
+
saved: false;
|
|
90
|
+
}) => void;
|
|
91
|
+
}
|
|
92
|
+
export interface SecretCardDeps {
|
|
93
|
+
readonly surfaceExchanges: Pick<SurfaceExchangeStore, 'open'>;
|
|
94
|
+
readonly askThenReport: SurfaceAskThenReport;
|
|
95
|
+
}
|
|
96
|
+
export type SecretCardHandler<Result> = (required: ToolExecutionContext, optional?: ToolExecutionOptions) => Promise<Result>;
|
|
97
|
+
export interface SecretCardTool<Prep, Result> {
|
|
98
|
+
readonly toolId: string;
|
|
99
|
+
secretFieldNames(prep: Prep): string[];
|
|
100
|
+
handler(deps: SecretCardDeps): SecretCardHandler<Result>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Define a secret card once; inject the exchange store and daemon askThenReport at composition.
|
|
104
|
+
* @param required - Domain fields, permission/target checks, save port and safe projections.
|
|
105
|
+
* @param options - Optional copy, error allowlist, fixed-metadata logger and resource framing.
|
|
106
|
+
* @returns Tool identity, dynamic secret-field metadata and a typed registry-compatible handler.
|
|
107
|
+
* @throws ToolInputError if the emitter is missing or fields violate the secret/routing contract.
|
|
108
|
+
* Prepare/transport failures propagate; save failures become a fixed safe outcome.
|
|
109
|
+
* @complexity O(n + h + b) locally in fields, rendered HTML and submitted secret bytes, plus host I/O.
|
|
110
|
+
* @example
|
|
111
|
+
* const tool = defineSecretCardTool(spec);
|
|
112
|
+
* registry.register({ descriptor, policy, handler: tool.handler({ surfaceExchanges, askThenReport }) });
|
|
113
|
+
*/
|
|
114
|
+
export declare function defineSecretCardTool<Prep, Saved, Result>(required: SecretCardSpec<Prep, Saved, Result>, options?: SecretCardOptions): SecretCardTool<Prep, Result>;
|
|
115
|
+
//# sourceMappingURL=secret-card.d.ts.map
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One human-only secret-card lifecycle. Keystrokes go directly to the held exchange and save
|
|
3
|
+
* port, never through model input, chat text, or logs. Saved values must stay in the host's
|
|
4
|
+
* sealed store; save returns only a safe summary for result/outcome projections.
|
|
5
|
+
*
|
|
6
|
+
* Fail closed without an emitter: returning a form or accepting a second model call would
|
|
7
|
+
* strand the held call or route a secret through model context. Cancel posts a dismissal so
|
|
8
|
+
* the call wakes immediately instead of waiting for its TTL. askThenReport keeps the exchange
|
|
9
|
+
* open until the real save finishes; reusing the form URI replaces premature "Done." with its
|
|
10
|
+
* actual outcome. The host supplies the daemon's existing helper, never a second store.
|
|
11
|
+
*/
|
|
12
|
+
import { ToolInputError, redactSecrets, SURFACE_DISMISSED_PARAM, SURFACE_EXCHANGE_ID_PARAM, SURFACE_TYPED_ANSWER_PARAM, } from '@jini-ai/core';
|
|
13
|
+
import { buildFormSurface } from './surfaces/form.js';
|
|
14
|
+
import { buildOutcomeSurface } from './surfaces/outcome.js';
|
|
15
|
+
/** Reject ambiguous or protocol-reserved names and runtime attempts to prefill secrets.
|
|
16
|
+
* @complexity O(n) time/space in the field count. Throws fixed ToolInputError text only. */
|
|
17
|
+
function checkedFields(form) {
|
|
18
|
+
const names = new Set([SURFACE_EXCHANGE_ID_PARAM, SURFACE_DISMISSED_PARAM, SURFACE_TYPED_ANSWER_PARAM]);
|
|
19
|
+
for (const field of form.fields) {
|
|
20
|
+
if (!field.name || names.has(field.name))
|
|
21
|
+
throw new ToolInputError({ message: 'Secret card field names must be unique and cannot use exchange parameters.' });
|
|
22
|
+
names.add(field.name);
|
|
23
|
+
if (field.secret === true && field.value !== undefined)
|
|
24
|
+
throw new ToolInputError({ message: 'Secret fields cannot have a pre-filled value. Omit value.' });
|
|
25
|
+
}
|
|
26
|
+
// Snapshot policies before waiting: host mutation must not change which submitted fields are secret.
|
|
27
|
+
return form.fields.map(field => ({ ...field }));
|
|
28
|
+
}
|
|
29
|
+
/** Classify before persistence; an abort wins over an already-buffered answer.
|
|
30
|
+
* Trimming tests blankness only: rewriting a provider secret can corrupt intentional newlines.
|
|
31
|
+
* @complexity O(n + b) before save in fields and submitted secret bytes; failure text delegates
|
|
32
|
+
* to core's exact-secret redaction scan. Host persistence cost is external. */
|
|
33
|
+
async function saveAnswer(required) {
|
|
34
|
+
const { answer, fields, prep, ctx, spec, options, exchangeId } = required;
|
|
35
|
+
if (ctx.signal.aborted)
|
|
36
|
+
return { status: 'abandoned' };
|
|
37
|
+
if (answer.status !== 'received')
|
|
38
|
+
return { status: answer.status };
|
|
39
|
+
if (answer.params[SURFACE_DISMISSED_PARAM] === true)
|
|
40
|
+
return { status: 'cancelled' };
|
|
41
|
+
const values = Object.fromEntries(fields.map(field => [field.name, answer.params[field.name]]));
|
|
42
|
+
const secrets = [];
|
|
43
|
+
for (const field of fields) {
|
|
44
|
+
if (field.secret !== true)
|
|
45
|
+
continue;
|
|
46
|
+
const value = values[field.name];
|
|
47
|
+
if (value !== undefined && typeof value !== 'string')
|
|
48
|
+
return { status: 'blank' };
|
|
49
|
+
const secret = typeof value === 'string' ? value : '';
|
|
50
|
+
if (secret.trim() === '' && (secret !== '' || field.allowBlank !== true))
|
|
51
|
+
return { status: 'blank' };
|
|
52
|
+
values[field.name] = secret;
|
|
53
|
+
secrets.push(secret);
|
|
54
|
+
}
|
|
55
|
+
if (ctx.signal.aborted)
|
|
56
|
+
return { status: 'abandoned' };
|
|
57
|
+
try {
|
|
58
|
+
return { status: 'saved', saved: await spec.save({ values, prep, signal: ctx.signal }) };
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (ctx.signal.aborted)
|
|
62
|
+
return { status: 'abandoned' };
|
|
63
|
+
let safeMessage = options.text?.saveFailure ?? 'Saving failed. Nothing was saved.';
|
|
64
|
+
try {
|
|
65
|
+
safeMessage = options.safeError?.(error) ?? safeMessage;
|
|
66
|
+
}
|
|
67
|
+
catch { /* An error mapper must not disclose a second exception containing the submitted value. */ }
|
|
68
|
+
// Even an accidentally permissive allowlist must not echo the submitted secret.
|
|
69
|
+
safeMessage = redactSecrets({ input: safeMessage }, { exactSecrets: secrets });
|
|
70
|
+
try {
|
|
71
|
+
options.logFailure?.({ toolId: spec.toolId, exchangeId, saved: false });
|
|
72
|
+
}
|
|
73
|
+
catch { /* Logging must not replace the safe model result with an opaque logger exception. */ }
|
|
74
|
+
return { status: 'failed', safeMessage };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Define a secret card once; inject the exchange store and daemon askThenReport at composition.
|
|
79
|
+
* @param required - Domain fields, permission/target checks, save port and safe projections.
|
|
80
|
+
* @param options - Optional copy, error allowlist, fixed-metadata logger and resource framing.
|
|
81
|
+
* @returns Tool identity, dynamic secret-field metadata and a typed registry-compatible handler.
|
|
82
|
+
* @throws ToolInputError if the emitter is missing or fields violate the secret/routing contract.
|
|
83
|
+
* Prepare/transport failures propagate; save failures become a fixed safe outcome.
|
|
84
|
+
* @complexity O(n + h + b) locally in fields, rendered HTML and submitted secret bytes, plus host I/O.
|
|
85
|
+
* @example
|
|
86
|
+
* const tool = defineSecretCardTool(spec);
|
|
87
|
+
* registry.register({ descriptor, policy, handler: tool.handler({ surfaceExchanges, askThenReport }) });
|
|
88
|
+
*/
|
|
89
|
+
export function defineSecretCardTool(required, options = {}) {
|
|
90
|
+
const spec = required;
|
|
91
|
+
return {
|
|
92
|
+
toolId: spec.toolId,
|
|
93
|
+
secretFieldNames: prep => checkedFields(spec.form({ prep })).filter(field => field.secret === true).map(field => field.name),
|
|
94
|
+
handler: deps => async (ctx, optional = {}) => {
|
|
95
|
+
if (!optional.emitSurface)
|
|
96
|
+
throw new ToolInputError({ message: options.text?.noEmitter ?? `${spec.toolId}: this execution context has no interactive form channel (no emitSurface), so this form cannot be shown here. Nothing was changed.` });
|
|
97
|
+
const prep = await spec.prepare({ ctx });
|
|
98
|
+
if (ctx.signal.aborted)
|
|
99
|
+
return spec.result({ prep, run: { status: 'abandoned' } });
|
|
100
|
+
const form = spec.form({ prep });
|
|
101
|
+
const fields = checkedFields(form);
|
|
102
|
+
if (ctx.signal.aborted)
|
|
103
|
+
return spec.result({ prep, run: { status: 'abandoned' } });
|
|
104
|
+
const exchange = deps.surfaceExchanges.open({ binding: { toolId: spec.toolId, principalId: ctx.principal.id, channel: 'mcp-ui' }, emit: optional.emitSurface });
|
|
105
|
+
const closeOnAbort = () => exchange.close({});
|
|
106
|
+
ctx.signal.addEventListener('abort', closeOnAbort, { once: true });
|
|
107
|
+
try {
|
|
108
|
+
// Close the open/register race as well as the prepare/abort race.
|
|
109
|
+
if (ctx.signal.aborted)
|
|
110
|
+
return spec.result({ prep, run: { status: 'abandoned' } });
|
|
111
|
+
const uri = `ui://${options.uriHost ?? 'jini'}/secret-card/${encodeURIComponent(spec.toolId)}/${encodeURIComponent(exchange.id)}`;
|
|
112
|
+
const framing = options.frameSize === undefined ? {} : { preferredFrameSize: options.frameSize };
|
|
113
|
+
const resource = buildFormSurface({ ...form, fields, uri, ...framing, toolName: spec.toolId,
|
|
114
|
+
baseParams: { [SURFACE_EXCHANGE_ID_PARAM]: exchange.id },
|
|
115
|
+
cancel: { label: form.cancelLabel ?? options.text?.cancelLabel ?? 'Cancel', toolName: spec.toolId,
|
|
116
|
+
params: { [SURFACE_EXCHANGE_ID_PARAM]: exchange.id, [SURFACE_DISMISSED_PARAM]: true } },
|
|
117
|
+
});
|
|
118
|
+
return await deps.askThenReport({ exchange, confirmationEmission: { channel: 'mcp-ui', payload: { resource } },
|
|
119
|
+
handle: async (answer) => {
|
|
120
|
+
const run = await saveAnswer({ answer, fields, prep, ctx, spec, options, exchangeId: exchange.id });
|
|
121
|
+
const result = spec.result({ prep, run });
|
|
122
|
+
const outcome = spec.outcome({ prep, run });
|
|
123
|
+
if (outcome === undefined)
|
|
124
|
+
return { result };
|
|
125
|
+
return { result, outcome: { channel: 'mcp-ui', payload: { resource: buildOutcomeSurface({ ...outcome, uri, ...framing }) } } };
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
ctx.signal.removeEventListener('abort', closeOnAbort);
|
|
131
|
+
// Also covers rendering/projection failures before or inside askThenReport; close is idempotent.
|
|
132
|
+
exchange.close({});
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=secret-card.js.map
|
|
@@ -209,6 +209,11 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
209
209
|
}
|
|
210
210
|
onVisibilityChange();
|
|
211
211
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
212
|
+
// Published Jini started the dwell during document.write. If the iframe is hidden then, no
|
|
213
|
+
// timer is armed; becoming visible by sandbox initialization need not send visibilitychange
|
|
214
|
+
// to that new document. Re-check at the bridge ready boundary, preserving the original
|
|
215
|
+
// visibility, dwell, trusted-click and single-call gates. whenReady is already in every bridge.
|
|
216
|
+
api.whenReady(onVisibilityChange);
|
|
212
217
|
|
|
213
218
|
for (var i = 0; i < actionButtons.length; i++) {
|
|
214
219
|
actionButtons[i].addEventListener("click", onClick);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SelectOption } from './select.js';
|
|
2
|
+
import { type SecretInputText } from './text-input.js';
|
|
2
3
|
interface SurfaceFieldBase {
|
|
3
4
|
/** HTML `name`, DOM `id` suffix, and the params key the value is posted under. */
|
|
4
5
|
readonly name: string;
|
|
@@ -14,7 +15,8 @@ export interface StringField extends SurfaceFieldBase {
|
|
|
14
15
|
readonly placeholder?: string;
|
|
15
16
|
readonly multiline?: boolean;
|
|
16
17
|
readonly rows?: number;
|
|
17
|
-
/** Renders as a masked
|
|
18
|
+
/** Renders as a masked input or multiline textarea; pre-filled values are forbidden.
|
|
19
|
+
* See {@link TextInputProps.secret}. */
|
|
18
20
|
readonly secret?: boolean;
|
|
19
21
|
}
|
|
20
22
|
export interface NumberField extends SurfaceFieldBase {
|
|
@@ -60,10 +62,11 @@ export type SurfaceField = StringField | NumberField | BooleanField | EnumField
|
|
|
60
62
|
* Renders the control fragment for one field, dispatching on its kind.
|
|
61
63
|
*
|
|
62
64
|
* @param field - See {@link SurfaceField}.
|
|
65
|
+
* @param text - Optional host translations for secret reveal controls.
|
|
63
66
|
* @returns An HTML fragment.
|
|
64
67
|
* @complexity O(n) in the rendered length.
|
|
65
68
|
*/
|
|
66
|
-
export declare function renderFieldControl(field: SurfaceField): string;
|
|
69
|
+
export declare function renderFieldControl(field: SurfaceField, text?: Partial<SecretInputText>): string;
|
|
67
70
|
/** What the surface's inline script needs to read a field's value back and coerce it — the runtime half of {@link SurfaceField}, with the presentation stripped out. */
|
|
68
71
|
export interface FieldReadSpec {
|
|
69
72
|
readonly name: string;
|
|
@@ -32,14 +32,16 @@ function base(field) {
|
|
|
32
32
|
* Renders the control fragment for one field, dispatching on its kind.
|
|
33
33
|
*
|
|
34
34
|
* @param field - See {@link SurfaceField}.
|
|
35
|
+
* @param text - Optional host translations for secret reveal controls.
|
|
35
36
|
* @returns An HTML fragment.
|
|
36
37
|
* @complexity O(n) in the rendered length.
|
|
37
38
|
*/
|
|
38
|
-
export function renderFieldControl(field) {
|
|
39
|
+
export function renderFieldControl(field, text) {
|
|
39
40
|
switch (field.kind) {
|
|
40
41
|
case 'string':
|
|
41
42
|
return renderTextInput({
|
|
42
43
|
...base(field),
|
|
44
|
+
...text,
|
|
43
45
|
...(field.value === undefined ? {} : { value: field.value }),
|
|
44
46
|
...(field.placeholder === undefined ? {} : { placeholder: field.placeholder }),
|
|
45
47
|
...(field.multiline === undefined ? {} : { multiline: field.multiline }),
|
|
@@ -28,8 +28,11 @@ import { type SurfaceDetail, type SurfaceStatusText } from './document.js';
|
|
|
28
28
|
import { type SurfaceField } from './fields.js';
|
|
29
29
|
import type { BridgeScriptSpec } from './bridge.js';
|
|
30
30
|
import type { SurfaceTokenName } from './tokens.js';
|
|
31
|
+
import { type SecretInputText } from './text-input.js';
|
|
31
32
|
export interface FormSurfaceSpec {
|
|
32
33
|
readonly title: string;
|
|
34
|
+
/** Resource framing carried through host form projections; ignored by HTML rendering. */
|
|
35
|
+
readonly preferredFrameSize?: readonly [string, string];
|
|
33
36
|
readonly description?: string;
|
|
34
37
|
/** Read-only context shown above the fields — what the input is *about*. */
|
|
35
38
|
readonly details?: readonly SurfaceDetail[];
|
|
@@ -52,7 +55,7 @@ export interface FormSurfaceSpec {
|
|
|
52
55
|
readonly toolName?: string;
|
|
53
56
|
readonly params?: Readonly<Record<string, unknown>>;
|
|
54
57
|
};
|
|
55
|
-
readonly text?: Partial<SurfaceStatusText>;
|
|
58
|
+
readonly text?: Partial<SurfaceStatusText & SecretInputText>;
|
|
56
59
|
readonly app?: BridgeScriptSpec;
|
|
57
60
|
readonly lang?: string;
|
|
58
61
|
readonly tokens?: Partial<Record<SurfaceTokenName, string>>;
|
|
@@ -27,6 +27,7 @@ import { createUIResource } from '../resource.js';
|
|
|
27
27
|
import { escapeHtml, escapeJsValue } from '../escape.js';
|
|
28
28
|
import { DEFAULT_SURFACE_STATUS_TEXT, SURFACE_SCRIPT_PRELUDE, renderActions, renderDetailList, renderStatusRegion, renderSurfaceDocument, renderSurfaceHeader, } from './document.js';
|
|
29
29
|
import { renderFieldControl, toFieldReadSpecs } from './fields.js';
|
|
30
|
+
import { DEFAULT_SECRET_INPUT_TEXT } from './text-input.js';
|
|
30
31
|
const DEFAULT_APP = { appName: 'jini-mcp-ui-form', appVersion: '1' };
|
|
31
32
|
/** The form element's id, fixed so the emitted script can find it without a fragile selector. */
|
|
32
33
|
const FORM_ELEMENT_ID = 'mcpui-form';
|
|
@@ -38,7 +39,14 @@ const FORM_ELEMENT_ID = 'mcpui-form';
|
|
|
38
39
|
* @complexity O(n) in the number of fields plus the rendered length.
|
|
39
40
|
*/
|
|
40
41
|
export function renderFormDocument(spec) {
|
|
41
|
-
|
|
42
|
+
// A base param with a secret field's name would leak the same value through the emitted script,
|
|
43
|
+
// even though the collected value overrides it at submit time. Reject that hidden prefill too.
|
|
44
|
+
for (const field of spec.fields) {
|
|
45
|
+
if (field.kind === 'string' && field.secret === true && spec.baseParams?.[field.name] !== undefined) {
|
|
46
|
+
throw new Error('Secret fields cannot have a pre-filled value. Omit value.');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...DEFAULT_SECRET_INPUT_TEXT, ...spec.text };
|
|
42
50
|
// Not `type: 'submit'` — see the runtime script below for why a submit-typed button inside this
|
|
43
51
|
// particular `<form>` cannot work at all in production.
|
|
44
52
|
const actions = [
|
|
@@ -46,7 +54,9 @@ export function renderFormDocument(spec) {
|
|
|
46
54
|
];
|
|
47
55
|
if (spec.cancel !== undefined)
|
|
48
56
|
actions.push({ id: 'cancel', label: spec.cancel.label, variant: 'neutral' });
|
|
49
|
-
const controls = spec.fields
|
|
57
|
+
const controls = spec.fields
|
|
58
|
+
.map((field) => renderFieldControl(field, { showSecret: text.showSecret, hideSecret: text.hideSecret }))
|
|
59
|
+
.join('\n');
|
|
50
60
|
const warning = spec.warning === undefined ? '' : `<p class="mcpui-warning">${escapeHtml(spec.warning)}</p>`;
|
|
51
61
|
const bodyHtml = [
|
|
52
62
|
renderSurfaceHeader(spec.description === undefined ? { title: spec.title } : { title: spec.title, description: spec.description }),
|
|
@@ -69,6 +79,57 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
69
79
|
var TEXT = ${escapeJsValue(text)};
|
|
70
80
|
var form = document.getElementById(${escapeJsValue(FORM_ELEMENT_ID)});
|
|
71
81
|
|
|
82
|
+
// A textarea normalizes CRLF/CR to LF even on .value assignment. Keep the original trusted paste
|
|
83
|
+
// privately, outside DOM/HTML, so multiline credentials reach the tool with their original bytes.
|
|
84
|
+
// An edit that changes the DOM value makes the snapshot ineligible; typed text then follows the
|
|
85
|
+
// native textarea value. Offsets count CRLF as one visible newline when a second paste replaces
|
|
86
|
+
// a selection, otherwise the original raw string would be spliced at the wrong position.
|
|
87
|
+
var secretPastes = new WeakMap();
|
|
88
|
+
function rawOffset(raw, visibleOffset) {
|
|
89
|
+
var rawIndex = 0;
|
|
90
|
+
var visibleIndex = 0;
|
|
91
|
+
while (rawIndex < raw.length && visibleIndex < visibleOffset) {
|
|
92
|
+
if (raw.charCodeAt(rawIndex) === 13 && raw.charCodeAt(rawIndex + 1) === 10) rawIndex++;
|
|
93
|
+
rawIndex++;
|
|
94
|
+
visibleIndex++;
|
|
95
|
+
}
|
|
96
|
+
return rawIndex;
|
|
97
|
+
}
|
|
98
|
+
form.addEventListener("paste", function (event) {
|
|
99
|
+
var control = event.target;
|
|
100
|
+
if (event.isTrusted !== true || !control || control.tagName !== "TEXTAREA" || !control.hasAttribute("data-mcpui-secret") || !event.clipboardData) return;
|
|
101
|
+
var pasted = event.clipboardData.getData("text/plain");
|
|
102
|
+
if (!pasted) return;
|
|
103
|
+
var snapshot = secretPastes.get(control);
|
|
104
|
+
var raw = snapshot && snapshot.normalized === control.value ? snapshot.raw : control.value;
|
|
105
|
+
var start = control.selectionStart;
|
|
106
|
+
var end = control.selectionEnd;
|
|
107
|
+
var next = raw.slice(0, rawOffset(raw, start)) + pasted + raw.slice(rawOffset(raw, end));
|
|
108
|
+
event.preventDefault();
|
|
109
|
+
control.value = next;
|
|
110
|
+
// Caret offsets use the displayed newline representation, independent of the saved raw text.
|
|
111
|
+
var caret = start + pasted.replace(/\\r\\n?/g, "\\n").length;
|
|
112
|
+
control.setSelectionRange(caret, caret);
|
|
113
|
+
secretPastes.set(control, { raw: next, normalized: control.value });
|
|
114
|
+
control.dispatchEvent(new Event("input", { bubbles: true }));
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// A reveal button only changes presentation: replacing a textarea with a password input would
|
|
118
|
+
// discard line breaks. Listen on the form so clicks on the button's contents work too.
|
|
119
|
+
form.addEventListener("click", function (event) {
|
|
120
|
+
if (event.isTrusted !== true) return;
|
|
121
|
+
var target = event.target;
|
|
122
|
+
if (!target || typeof target.closest !== "function") return;
|
|
123
|
+
var button = target.closest("button[data-mcpui-secret-toggle]");
|
|
124
|
+
if (!button || button.disabled) return;
|
|
125
|
+
var control = document.getElementById(button.getAttribute("data-mcpui-secret-toggle"));
|
|
126
|
+
if (!control) return;
|
|
127
|
+
var reveal = button.getAttribute("aria-pressed") !== "true";
|
|
128
|
+
control.setAttribute("style", "-webkit-text-security: " + (reveal ? "none" : "disc") + ";");
|
|
129
|
+
button.setAttribute("aria-pressed", reveal ? "true" : "false");
|
|
130
|
+
button.textContent = button.getAttribute(reveal ? "data-mcpui-hide-secret" : "data-mcpui-show-secret");
|
|
131
|
+
});
|
|
132
|
+
|
|
72
133
|
// form.elements[name] yields a RadioNodeList when several controls share a name and the BARE
|
|
73
134
|
// element when only one does. A one-option checklist would otherwise read as "no options" —
|
|
74
135
|
// silently, since a bare input has no length to iterate. Presence of "checked" is what tells the
|
|
@@ -88,7 +149,8 @@ ${SURFACE_SCRIPT_PRELUDE}
|
|
|
88
149
|
if (field.kind === "multi-enum") return checkedValues(control);
|
|
89
150
|
// Covers a radio-presented enum too: a RadioNodeList's own .value is the checked radio's
|
|
90
151
|
// value, or "" when none is checked — the same shape a <select> reports.
|
|
91
|
-
|
|
152
|
+
var snapshot = secretPastes.get(control);
|
|
153
|
+
return snapshot && snapshot.normalized === control.value ? snapshot.raw : control.value;
|
|
92
154
|
}
|
|
93
155
|
|
|
94
156
|
function isBlank(field, value) {
|
|
@@ -48,6 +48,8 @@ import type { BridgeScriptSpec } from './bridge.js';
|
|
|
48
48
|
import type { SurfaceTokenName } from './tokens.js';
|
|
49
49
|
export interface SurfaceOutcomeSpec {
|
|
50
50
|
readonly title: string;
|
|
51
|
+
/** Resource framing carried through host outcome projections; ignored by HTML rendering. */
|
|
52
|
+
readonly preferredFrameSize?: readonly [string, string];
|
|
51
53
|
readonly description?: string;
|
|
52
54
|
/** The facts of what actually happened — same shape as a confirmation's own `details`, but
|
|
53
55
|
* reporting the RESULT (e.g. the real URL, the real status) rather than what was proposed. */
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
/** Labels the host can translate for a multiline secret's reveal toggle. */
|
|
2
|
+
export interface SecretInputText {
|
|
3
|
+
readonly showSecret: string;
|
|
4
|
+
readonly hideSecret: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const DEFAULT_SECRET_INPUT_TEXT: SecretInputText;
|
|
7
|
+
export interface TextInputProps extends Partial<SecretInputText> {
|
|
2
8
|
/** HTML `name`, DOM `id` suffix, and the params key the value is posted under. */
|
|
3
9
|
readonly name: string;
|
|
4
10
|
readonly label: string;
|
|
5
|
-
/** Pre-filled value. Numbers are stringified; `undefined` leaves the control empty.
|
|
11
|
+
/** Pre-filled value. Numbers are stringified; `undefined` leaves the control empty.
|
|
12
|
+
* Forbidden when `secret` is true, including an empty string. */
|
|
6
13
|
readonly value?: string | number;
|
|
7
14
|
readonly placeholder?: string;
|
|
8
15
|
/** Help text, wired up with `aria-describedby`. */
|
|
@@ -11,10 +18,11 @@ export interface TextInputProps {
|
|
|
11
18
|
readonly disabled?: boolean;
|
|
12
19
|
/** `'number'` also constrains the on-screen keyboard on touch devices, which `'text'` would not. */
|
|
13
20
|
readonly inputType?: 'text' | 'number';
|
|
14
|
-
/** Renders as `<input type="password"
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* same precedence `multiline`'s own doc comment already gives `inputType: 'number'`.
|
|
21
|
+
/** Renders as `<input type="password">`, or a masked textarea with a show/hide toggle when
|
|
22
|
+
* multiline. No `<textarea type="password">` exists, so multiline uses CSS text security.
|
|
23
|
+
* Ignored for presentation when `inputType` is `'number'` (no such thing as a masked number) —
|
|
24
|
+
* same precedence `multiline`'s own doc comment already gives `inputType: 'number'`.
|
|
25
|
+
* A supplied value is always rejected: masking cannot protect a secret serialized into HTML. */
|
|
18
26
|
readonly secret?: boolean;
|
|
19
27
|
/** Renders a `<textarea>`. Ignored when `inputType` is `'number'` — there is no multiline number. */
|
|
20
28
|
readonly multiline?: boolean;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { escapeHtml } from '../escape.js';
|
|
9
9
|
import { fieldDescribedBy, fieldElementId, renderFieldLabel } from './document.js';
|
|
10
|
+
export const DEFAULT_SECRET_INPUT_TEXT = { showSecret: 'Show', hideSecret: 'Hide' };
|
|
10
11
|
function optionalAttribute(name, value) {
|
|
11
12
|
if (value === undefined)
|
|
12
13
|
return '';
|
|
@@ -36,6 +37,11 @@ function resolveInputType(props) {
|
|
|
36
37
|
* @complexity O(n) in the rendered length.
|
|
37
38
|
*/
|
|
38
39
|
export function renderTextInput(props) {
|
|
40
|
+
// Even an empty prefill is forbidden: callers must omit value so a stored credential can never
|
|
41
|
+
// enter the resource's HTML. Keep this error fixed, without the field name or rejected value.
|
|
42
|
+
if (props.secret === true && props.value !== undefined) {
|
|
43
|
+
throw new Error('Secret fields cannot have a pre-filled value. Omit value.');
|
|
44
|
+
}
|
|
39
45
|
const id = fieldElementId(props.name);
|
|
40
46
|
const common = ` id="${escapeHtml(id)}" name="${escapeHtml(props.name)}"` +
|
|
41
47
|
optionalAttribute('placeholder', props.placeholder) +
|
|
@@ -44,8 +50,16 @@ export function renderTextInput(props) {
|
|
|
44
50
|
fieldDescribedBy(props);
|
|
45
51
|
const type = resolveInputType(props);
|
|
46
52
|
const isNumber = type === 'number';
|
|
47
|
-
const
|
|
48
|
-
|
|
53
|
+
const secretMultiline = props.secret === true && props.multiline === true && !isNumber;
|
|
54
|
+
const control = props.multiline === true && !isNumber
|
|
55
|
+
? `<textarea class="mcpui-textarea"${common}${optionalAttribute('rows', props.rows)}` +
|
|
56
|
+
// Password inputs strip line breaks. CSS masking keeps the textarea's actual value intact,
|
|
57
|
+
// and disables browser text assistance that could retain or alter a pasted credential.
|
|
58
|
+
(secretMultiline ? ' data-mcpui-secret style="-webkit-text-security: disc;" autocomplete="off" spellcheck="false"' : '') +
|
|
59
|
+
`>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>` +
|
|
60
|
+
(secretMultiline
|
|
61
|
+
? `<button type="button" class="mcpui-button" data-mcpui-secret-toggle="${escapeHtml(id)}" data-mcpui-show-secret="${escapeHtml(props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret)}" data-mcpui-hide-secret="${escapeHtml(props.hideSecret ?? DEFAULT_SECRET_INPUT_TEXT.hideSecret)}" aria-controls="${escapeHtml(id)}" aria-pressed="false"${booleanAttribute('disabled', props.disabled)}>${escapeHtml(props.showSecret ?? DEFAULT_SECRET_INPUT_TEXT.showSecret)}</button>`
|
|
62
|
+
: '')
|
|
49
63
|
: `<input class="mcpui-input" type="${type}"${common}` +
|
|
50
64
|
optionalAttribute('value', props.value) +
|
|
51
65
|
// A stored secret (e.g. a cloud provider's access key or a database password) must never be
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { Connector, ConnectorStatusMap } from '../../../connectors/types.js';
|
|
2
2
|
import type { ConnectorMemoryAttempt, MemoryExtractionRecord, MemorySuggestion } from '../../types.js';
|
|
3
3
|
export declare function MemoryConnectedPanel({ enabled, onOpenConnectors, connectorStatuses, connectorsLoading, connectedCount, selectedConnectorIds, selectedConnectedConnectorIds, connectingConnectorIds, pendingConnectorAuthIds, connectorConnectErrors, connectorIdsWithDetails, connectorExtracting, connectorSaving, connectorScanLabel, connectorSuggestions, selectedSuggestionIds, selectedConnectorSuggestions, connectorStatus, connectorError, connectorLoadError, connectorAttempts, connectorContextBytes, connectorExtractions, memoryConnectors, toggleConnectorSelection, onConnectMemoryConnector, toggleConnectorSuggestion, onSuggestConnectorMemory, onSaveConnectorSuggestions, onDiscardConnectorSuggestions, nowClock, onOpenPreview, onDeleteExtraction, }: {
|
|
4
4
|
enabled: boolean;
|
|
@@ -9,7 +9,6 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
9
9
|
// concern was already dropped when the connectors canary shipped; a host
|
|
10
10
|
// resolving theme-swapped logo URLs does so before handing `logoUrl` in).
|
|
11
11
|
import { useMemo } from 'react';
|
|
12
|
-
import {} from '../../../connectors/types.js';
|
|
13
12
|
import { connectorAttemptDetail, connectorAttemptTitle, formatConnectorContextBytes, memoryTypeLabels, } from '../../formatters.js';
|
|
14
13
|
import { ConnectorLogo } from '../../../connectors/index.js';
|
|
15
14
|
import { Icon } from '../../../../react/components/Icon.js';
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralize the
|
|
3
|
-
* the stored timestamp's prefix byte for byte
|
|
2
|
+
* Centralize the fixed-width display so formatting changes have one owner. The default
|
|
3
|
+
* preserves the stored timestamp's prefix byte for byte, without parsing or timezone conversion.
|
|
4
4
|
* Trailing seconds, milliseconds and zone suffix are intentionally dropped; callers supply ISO text.
|
|
5
5
|
*/
|
|
6
|
-
/** Render
|
|
6
|
+
/** Render YYYY-MM-DD HH:MM; hosts can select the viewer's local timezone explicitly. */
|
|
7
7
|
export declare function formatTimestamp({ iso }: {
|
|
8
8
|
iso: string;
|
|
9
|
+
}, { timeZone }?: {
|
|
10
|
+
timeZone?: "stored" | "local";
|
|
9
11
|
}): string;
|
|
10
12
|
/** Inject 'now' to keep relative-time display deterministic. The fallback wording uses simple
|
|
11
13
|
* English singular/plural templates; hosts can translate them, but this is not full locale pluralization. */
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Centralize the
|
|
3
|
-
* the stored timestamp's prefix byte for byte
|
|
2
|
+
* Centralize the fixed-width display so formatting changes have one owner. The default
|
|
3
|
+
* preserves the stored timestamp's prefix byte for byte, without parsing or timezone conversion.
|
|
4
4
|
* Trailing seconds, milliseconds and zone suffix are intentionally dropped; callers supply ISO text.
|
|
5
5
|
*/
|
|
6
|
-
/** Render
|
|
7
|
-
export function formatTimestamp({ iso }) {
|
|
6
|
+
/** Render YYYY-MM-DD HH:MM; hosts can select the viewer's local timezone explicitly. */
|
|
7
|
+
export function formatTimestamp({ iso }, { timeZone = "stored" } = {}) {
|
|
8
|
+
if (timeZone === "local") {
|
|
9
|
+
// A stored UTC prefix can label server time as viewer time (Tovu D-06). Local getters
|
|
10
|
+
// follow the browser's timezone and DST; zone-less ISO is already local wall time.
|
|
11
|
+
const date = new Date(iso);
|
|
12
|
+
if (Number.isNaN(date.getTime()))
|
|
13
|
+
return iso; // Keep malformed source data visible.
|
|
14
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
15
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
16
|
+
}
|
|
8
17
|
return iso.slice(0, 16).replace("T", " ");
|
|
9
18
|
}
|
|
10
19
|
/** Inject 'now' to keep relative-time display deterministic. The fallback wording uses simple
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
// ADR-021: GET /auth/me flattens effectivePermissions, losing resourceType/constraintJson.
|
|
2
|
+
// ADR-006 keeps one server evaluator; every mutation and tool call must be rechecked there.
|
|
1
3
|
/** UI affordance check for exact grants or the literal * wildcard; server authorization remains host-owned. */
|
|
2
4
|
/**
|
|
3
5
|
* A flattened permission list has lost resource scopes and constraints. Treating '*' as an ordinary
|