@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.
Files changed (169) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +20 -0
  3. package/dist/core/theme/default.d.ts +4 -0
  4. package/dist/core/theme/default.js +11 -0
  5. package/dist/core/theme/stylesheet.d.ts +8 -0
  6. package/dist/core/theme/stylesheet.js +14 -0
  7. package/dist/core/theme/types.d.ts +39 -0
  8. package/dist/core/theme/types.js +2 -0
  9. package/dist/core/theme/validation.d.ts +15 -0
  10. package/dist/core/theme/validation.js +55 -0
  11. package/dist/features/admin-widgets/components/Select/Select.d.ts +11 -3
  12. package/dist/features/admin-widgets/components/Select/Select.hooks.d.ts +8 -13
  13. package/dist/features/admin-widgets/components/Select/Select.hooks.js +37 -215
  14. package/dist/features/admin-widgets/components/Select/Select.js +4 -3
  15. package/dist/features/appearance/constants.js +1 -1
  16. package/dist/features/connectors/rules.d.ts +1 -6
  17. package/dist/features/connectors/types.d.ts +1 -6
  18. package/dist/features/connectors/types.js +1 -6
  19. package/dist/features/execution/http-adapter.d.ts +111 -0
  20. package/dist/features/execution/http-adapter.js +160 -0
  21. package/dist/features/execution/index.d.ts +4 -0
  22. package/dist/features/execution/index.js +2 -0
  23. package/dist/features/execution/react/hooks/use-execution-credential.hooks.d.ts +232 -0
  24. package/dist/features/execution/react/hooks/use-execution-credential.hooks.js +217 -0
  25. package/dist/features/html-editor/canvas-style.d.ts +1 -1
  26. package/dist/features/html-editor/canvas-style.js +2 -2
  27. package/dist/features/i18n/dictionaries/settings-dialog.ar.js +2 -0
  28. package/dist/features/i18n/dictionaries/settings-dialog.bn.js +2 -0
  29. package/dist/features/i18n/dictionaries/settings-dialog.de.js +2 -0
  30. package/dist/features/i18n/dictionaries/settings-dialog.en.js +2 -0
  31. package/dist/features/i18n/dictionaries/settings-dialog.es.js +2 -0
  32. package/dist/features/i18n/dictionaries/settings-dialog.fa.js +2 -0
  33. package/dist/features/i18n/dictionaries/settings-dialog.fr.js +2 -0
  34. package/dist/features/i18n/dictionaries/settings-dialog.hi.js +2 -0
  35. package/dist/features/i18n/dictionaries/settings-dialog.hu.js +2 -0
  36. package/dist/features/i18n/dictionaries/settings-dialog.id.js +2 -0
  37. package/dist/features/i18n/dictionaries/settings-dialog.it.js +2 -0
  38. package/dist/features/i18n/dictionaries/settings-dialog.ja.js +2 -0
  39. package/dist/features/i18n/dictionaries/settings-dialog.ko.js +2 -0
  40. package/dist/features/i18n/dictionaries/settings-dialog.pl.js +2 -0
  41. package/dist/features/i18n/dictionaries/settings-dialog.pt-BR.js +2 -0
  42. package/dist/features/i18n/dictionaries/settings-dialog.ru.js +2 -0
  43. package/dist/features/i18n/dictionaries/settings-dialog.th.js +2 -0
  44. package/dist/features/i18n/dictionaries/settings-dialog.tr.js +2 -0
  45. package/dist/features/i18n/dictionaries/settings-dialog.uk.js +2 -0
  46. package/dist/features/i18n/dictionaries/settings-dialog.ur.js +2 -0
  47. package/dist/features/i18n/dictionaries/settings-dialog.zh-CN.js +2 -0
  48. package/dist/features/i18n/dictionaries/settings-dialog.zh-TW.js +2 -0
  49. package/dist/features/i18n/dictionary-translator.d.ts +5 -2
  50. package/dist/features/i18n/dictionary-translator.js +1 -1
  51. package/dist/features/i18n/index.d.ts +1 -1
  52. package/dist/features/i18n/index.js +1 -1
  53. package/dist/features/i18n/template-i18n.d.ts +11 -0
  54. package/dist/features/i18n/template-i18n.js +12 -0
  55. package/dist/features/interactive-ui/interactive-ui.css +1 -1
  56. package/dist/features/interactive-ui/providers/recharts/bar-chart.js +3 -2
  57. package/dist/features/interactive-ui/providers/recharts/chart-tokens.d.ts +43 -0
  58. package/dist/features/interactive-ui/providers/recharts/chart-tokens.js +44 -0
  59. package/dist/features/interactive-ui/providers/recharts/line-chart.js +3 -2
  60. package/dist/features/interactive-ui/providers/recharts/pie-chart.js +2 -2
  61. package/dist/features/lexical-rich-text-editor/deserialize.d.ts +0 -5
  62. package/dist/features/lexical-rich-text-editor/deserialize.js +0 -5
  63. package/dist/features/lexical-rich-text-editor/mention-node.d.ts +5 -17
  64. package/dist/features/lexical-rich-text-editor/mention-node.js +5 -17
  65. package/dist/features/lexical-rich-text-editor/mention-parser.d.ts +2 -4
  66. package/dist/features/lexical-rich-text-editor/serialize.d.ts +0 -4
  67. package/dist/features/lexical-rich-text-editor/serialize.js +0 -4
  68. package/dist/features/lexical-rich-text-editor/types.d.ts +3 -13
  69. package/dist/features/lexical-rich-text-editor/types.js +3 -13
  70. package/dist/features/mcp-ui/early-message-buffer.d.ts +1 -2
  71. package/dist/features/mcp-ui/early-message-buffer.js +1 -2
  72. package/dist/features/mcp-ui/escape.d.ts +4 -0
  73. package/dist/features/mcp-ui/escape.js +2 -9
  74. package/dist/features/mcp-ui/index.d.ts +1 -0
  75. package/dist/features/mcp-ui/index.js +1 -0
  76. package/dist/features/mcp-ui/protocol.d.ts +2 -5
  77. package/dist/features/mcp-ui/protocol.js +2 -5
  78. package/dist/features/mcp-ui/resource.d.ts +47 -7
  79. package/dist/features/mcp-ui/resource.js +5 -8
  80. package/dist/features/mcp-ui/secret-card.d.ts +115 -0
  81. package/dist/features/mcp-ui/secret-card.js +137 -0
  82. package/dist/features/mcp-ui/surfaces/confirmation.js +5 -0
  83. package/dist/features/mcp-ui/surfaces/fields.d.ts +5 -2
  84. package/dist/features/mcp-ui/surfaces/fields.js +3 -1
  85. package/dist/features/mcp-ui/surfaces/form.d.ts +4 -1
  86. package/dist/features/mcp-ui/surfaces/form.js +65 -3
  87. package/dist/features/mcp-ui/surfaces/outcome.d.ts +2 -0
  88. package/dist/features/mcp-ui/surfaces/text-input.d.ts +14 -6
  89. package/dist/features/mcp-ui/surfaces/text-input.js +16 -2
  90. package/dist/features/mcp-ui/surfaces/tokens.js +1 -1
  91. package/dist/features/memory/react/components/MemoryConnectedPanel.d.ts +1 -1
  92. package/dist/features/memory/react/components/MemoryConnectedPanel.js +0 -1
  93. package/dist/features/panel-kit/helpers/format-timestamp.d.ts +5 -3
  94. package/dist/features/panel-kit/helpers/format-timestamp.js +13 -4
  95. package/dist/features/panel-kit/helpers/permissions.js +2 -0
  96. package/dist/features/panel-kit/helpers/resolve-active-tab-id.js +2 -0
  97. package/dist/features/panel-kit/helpers/standing-draft-local-backup.d.ts +51 -0
  98. package/dist/features/panel-kit/helpers/standing-draft-local-backup.js +77 -0
  99. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.d.ts +8 -0
  100. package/dist/features/panel-kit/helpers/standing-draft-recovery-message.js +10 -0
  101. package/dist/features/panel-kit/hooks/use-async-action.hooks.d.ts +1 -0
  102. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.d.ts +1 -0
  103. package/dist/features/panel-kit/hooks/use-dirty-guard.hooks.js +1 -0
  104. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.d.ts +8 -0
  105. package/dist/features/panel-kit/hooks/use-serial-writes.hooks.js +3 -0
  106. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.d.ts +211 -0
  107. package/dist/features/panel-kit/hooks/use-settings-slice.hooks.js +382 -0
  108. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.d.ts +179 -0
  109. package/dist/features/panel-kit/hooks/use-standing-draft-autosave.hooks.js +300 -0
  110. package/dist/features/panel-kit/index.d.ts +7 -1
  111. package/dist/features/panel-kit/index.js +5 -1
  112. package/dist/features/resource-dashboard/types.d.ts +8 -41
  113. package/dist/features/resource-dashboard/types.js +3 -26
  114. package/dist/features/tab-strip/constants.d.ts +3 -4
  115. package/dist/features/tab-strip/constants.js +3 -4
  116. package/dist/features/tab-strip/dependencies.d.ts +1 -6
  117. package/dist/features/tab-strip/dependencies.js +1 -6
  118. package/dist/features/tab-strip/react/components/TabBar.d.ts +33 -1
  119. package/dist/features/tab-strip/react/components/TabBar.hooks.d.ts +57 -5
  120. package/dist/features/tab-strip/react/components/TabBar.hooks.js +63 -5
  121. package/dist/features/tab-strip/react/components/TabBar.js +12 -1
  122. package/dist/features/tab-strip/rules.d.ts +4 -17
  123. package/dist/features/tab-strip/rules.js +4 -17
  124. package/dist/features/tab-strip/types.d.ts +2 -13
  125. package/dist/icon-name.d.ts +1 -1
  126. package/dist/icon-name.js +1 -0
  127. package/dist/index.d.ts +42 -417
  128. package/dist/index.js +42 -430
  129. package/dist/react/components/CustomSelect.d.ts +6 -2
  130. package/dist/react/components/CustomSelect.js +20 -91
  131. package/dist/react/components/Icon.d.ts +6 -0
  132. package/dist/react/components/Icon.js +12 -0
  133. package/dist/react/components/select-popover.d.ts +106 -0
  134. package/dist/react/components/select-popover.js +210 -0
  135. package/dist/renderers/sandbox-scripts.d.ts +29 -0
  136. package/dist/renderers/sandbox-scripts.js +127 -0
  137. package/dist/renderers/sandboxed-document.js +3 -86
  138. package/dist/renderers/srcdoc/build.d.ts +10 -17
  139. package/dist/renderers/srcdoc/build.js +31 -234
  140. package/dist/styles/variables.css +30 -0
  141. package/dist/theme/apply.d.ts +1 -1
  142. package/dist/theme/apply.js +2 -2
  143. package/dist/theme/color-scheme.d.ts +1 -1
  144. package/dist/theme/default.d.ts +1 -3
  145. package/dist/theme/default.js +2 -10
  146. package/dist/theme/index.d.ts +3 -3
  147. package/dist/theme/index.js +2 -2
  148. package/dist/theme/stylesheet.d.ts +1 -7
  149. package/dist/theme/stylesheet.js +2 -13
  150. package/dist/theme/types.d.ts +1 -38
  151. package/dist/theme/types.js +2 -1
  152. package/dist/theme/validation.d.ts +1 -14
  153. package/dist/theme/validation.js +2 -54
  154. package/dist/utils/endpoint-policy.d.ts +21 -39
  155. package/dist/utils/endpoint-policy.js +19 -107
  156. package/dist/utils/file-system-errors.d.ts +1 -2
  157. package/dist/utils/file-system-errors.js +1 -2
  158. package/dist/utils/ime-composing.d.ts +0 -3
  159. package/dist/utils/ime-composing.js +0 -3
  160. package/dist/utils/platform.d.ts +1 -4
  161. package/dist/utils/platform.js +1 -4
  162. package/dist/utils/smooth-scroll-to-top.d.ts +1 -3
  163. package/dist/utils/smooth-scroll-to-top.js +3 -13
  164. package/dist/utils/timezone.js +1 -5
  165. package/dist/utils/uuid.d.ts +0 -3
  166. package/dist/utils/uuid.js +0 -3
  167. package/dist/utils/visual-stability.d.ts +1 -3
  168. package/dist/utils/visual-stability.js +1 -3
  169. 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
- * Ported from the reference implementation's `mcp-ui.ts`, minus that file's hand-rolled copy of the
16
- * JSON-RPC envelope and its `UIActionResult` union: the reference implementation declared those
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 `<input type="password">` — see {@link TextInputProps.secret}. */
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
- const text = { ...DEFAULT_SURFACE_STATUS_TEXT, ...spec.text };
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.map(renderFieldControl).join('\n');
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
- return control.value;
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
- export interface TextInputProps {
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">` — the value is never visible on screen, and browser
15
- * password managers may offer to remember it. Ignored when `inputType` is `'number'` (no such
16
- * thing as a masked number) and forces `multiline` off (no `<textarea type="password">` exists) —
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 control = props.multiline === true && type === 'text'
48
- ? `<textarea class="mcpui-textarea"${common}${optionalAttribute('rows', props.rows)}>${props.value === undefined ? '' : escapeHtml(String(props.value))}</textarea>`
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 { defaultAdminTheme } from '../../../theme/default.js';
1
+ import { defaultAdminTheme } from '../../../core/theme/default.js';
2
2
  /**
3
3
  * @module features/mcp-ui/surfaces/tokens
4
4
  *
@@ -1,4 +1,4 @@
1
- import { type Connector, type ConnectorStatusMap } from '../../../connectors/types.js';
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 historical fixed-width display so formatting changes have one owner. Preserve
3
- * the stored timestamp's prefix byte for byte: no parsing, locale formatting or timezone conversion.
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 the ISO prefix as YYYY-MM-DD HH:MM without timezone conversion. */
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 historical fixed-width display so formatting changes have one owner. Preserve
3
- * the stored timestamp's prefix byte for byte: no parsing, locale formatting or timezone conversion.
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 the ISO prefix as YYYY-MM-DD HH:MM without timezone conversion. */
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