@ggui-ai/iframe-runtime 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,17 +39,26 @@ export declare const EMPTY_COMPILED_VALIDATOR_SET: CompiledValidatorSet;
39
39
  */
40
40
  export declare function loadCompiledValidators(meta: CompiledContractValidators | undefined, warn?: (message: string, detail: unknown) => void): Promise<CompiledValidatorSet>;
41
41
  /**
42
- * #109 content-addressable variant: fetch the validator-bundle URL,
43
- * dynamic-import the response, then delegate to
44
- * {@link loadCompiledValidators}. Returns {@link EMPTY_COMPILED_VALIDATOR_SET}
42
+ * #109 content-addressable variant: fetch the validator-bundle URL via
43
+ * ONE dynamic import. Returns {@link EMPTY_COMPILED_VALIDATOR_SET}
45
44
  * when `url` is undefined OR the fetch/import fails — sidecar
46
45
  * semantics, the server-side `assertActionContract` gate remains
47
46
  * authoritative on validator absence.
48
47
  *
49
- * Response shape: an ES module whose `default` export is a
50
- * {@link CompiledContractValidators} object (produced server-side by
51
- * `bundleCompiledValidatorsAsModule`). `Cache-Control: immutable` so
52
- * repeat renders with the same contract hit browser cache with no
48
+ * Response shape (v2 executable format, ggui#522 slice 2): an ES
49
+ * module whose `default` export carries the validate FUNCTIONS
50
+ * themselves (produced server-side by
51
+ * `bundleValidatorExprsAsExecutableModule`) — the single https import
52
+ * is the whole load, governed by `script-src` origins alone. No
53
+ * per-validator `blob:` import remains on this path, which is what
54
+ * lets a strict-CSP embedder (no `blob:`, no `data:`, no
55
+ * `'unsafe-eval'`) run client-side outbound validation at all.
56
+ *
57
+ * A default export whose leaves are not functions (the retired v1
58
+ * format carried module SOURCES as strings — reachable only through a
59
+ * stale CDN copy under a pre-salt URL, which no current slice mints)
60
+ * degrades to the empty set with a warning. `Cache-Control: immutable`
61
+ * so repeat renders with the same contract hit browser cache with no
53
62
  * round-trip.
54
63
  */
55
64
  export declare function loadCompiledValidatorsFromUrl(url: string | undefined, warn?: (message: string, detail: unknown) => void): Promise<CompiledValidatorSet>;
@@ -73,17 +73,26 @@ export async function loadCompiledValidators(meta, warn) {
73
73
  };
74
74
  }
75
75
  /**
76
- * #109 content-addressable variant: fetch the validator-bundle URL,
77
- * dynamic-import the response, then delegate to
78
- * {@link loadCompiledValidators}. Returns {@link EMPTY_COMPILED_VALIDATOR_SET}
76
+ * #109 content-addressable variant: fetch the validator-bundle URL via
77
+ * ONE dynamic import. Returns {@link EMPTY_COMPILED_VALIDATOR_SET}
79
78
  * when `url` is undefined OR the fetch/import fails — sidecar
80
79
  * semantics, the server-side `assertActionContract` gate remains
81
80
  * authoritative on validator absence.
82
81
  *
83
- * Response shape: an ES module whose `default` export is a
84
- * {@link CompiledContractValidators} object (produced server-side by
85
- * `bundleCompiledValidatorsAsModule`). `Cache-Control: immutable` so
86
- * repeat renders with the same contract hit browser cache with no
82
+ * Response shape (v2 executable format, ggui#522 slice 2): an ES
83
+ * module whose `default` export carries the validate FUNCTIONS
84
+ * themselves (produced server-side by
85
+ * `bundleValidatorExprsAsExecutableModule`) — the single https import
86
+ * is the whole load, governed by `script-src` origins alone. No
87
+ * per-validator `blob:` import remains on this path, which is what
88
+ * lets a strict-CSP embedder (no `blob:`, no `data:`, no
89
+ * `'unsafe-eval'`) run client-side outbound validation at all.
90
+ *
91
+ * A default export whose leaves are not functions (the retired v1
92
+ * format carried module SOURCES as strings — reachable only through a
93
+ * stale CDN copy under a pre-salt URL, which no current slice mints)
94
+ * degrades to the empty set with a warning. `Cache-Control: immutable`
95
+ * so repeat renders with the same contract hit browser cache with no
87
96
  * round-trip.
88
97
  */
89
98
  export async function loadCompiledValidatorsFromUrl(url, warn) {
@@ -96,7 +105,31 @@ export async function loadCompiledValidatorsFromUrl(url, warn) {
96
105
  warn?.('[ggui:validators] contract bundle: default export is not an object', compiled);
97
106
  return EMPTY_COMPILED_VALIDATOR_SET;
98
107
  }
99
- return loadCompiledValidators(compiled, warn);
108
+ const groups = compiled;
109
+ const pickGroup = (group, kind) => {
110
+ const map = new Map();
111
+ if (group === undefined)
112
+ return map;
113
+ for (const [name, fn] of Object.entries(group)) {
114
+ if (isValidateFunction(fn)) {
115
+ map.set(name, fn);
116
+ }
117
+ else {
118
+ warn?.(`[ggui:validators] ${kind}.${name}: bundle entry is not a function (stale v1-format bundle?)`, typeof fn);
119
+ }
120
+ }
121
+ return map;
122
+ };
123
+ const props = isValidateFunction(groups.props) ? groups.props : undefined;
124
+ if (groups.props !== undefined && props === undefined) {
125
+ warn?.('[ggui:validators] props: bundle entry is not a function (stale v1-format bundle?)', typeof groups.props);
126
+ }
127
+ return {
128
+ ...(props !== undefined ? { props } : {}),
129
+ actions: pickGroup(groups.actions, 'action'),
130
+ streams: pickGroup(groups.streams, 'stream'),
131
+ context: pickGroup(groups.context, 'context'),
132
+ };
100
133
  }
101
134
  catch (err) {
102
135
  warn?.(`[ggui:validators] contract bundle: failed to load ${url}`, err);
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Spec→ggui token bridge (ggui#572).
3
+ *
4
+ * MCP Apps hosts announce their palette as spec `--color-*` custom
5
+ * properties in `hostContext.styles.variables`. The canonical
6
+ * `@modelcontextprotocol/ext-apps` helper applies them as inline
7
+ * custom properties on the iframe `<html>` — which is correct spec
8
+ * behavior but repaints nothing ggui renders: generated UI, the
9
+ * design system, and the shell chrome consume exclusively `--ggui-*`
10
+ * tokens, and the renderer's scoped in-body token block shadows
11
+ * root-level inheritance for every token it defines anyway.
12
+ *
13
+ * This module translates the spec vocabulary onto the ggui token
14
+ * ladder so the renderer can merge the host's palette INTO the scoped
15
+ * block — as the fallback layer beneath the app's own theme (#573
16
+ * ruling: the slice's stamped theme wins; the host palette fills the
17
+ * slots no operator repainted).
18
+ *
19
+ * Coverage is deliberately PARTIAL in both directions:
20
+ *
21
+ * - Spec keys with no faithful ggui counterpart (ghost/disabled/
22
+ * inverse variants, ring colors, fonts, radii, shadows) are
23
+ * dropped — they keep applying via the ext-apps inline-DOM path
24
+ * for any non-ggui content, and inventing a mapping would repaint
25
+ * tokens with the WRONG semantic role.
26
+ * - ggui tokens the spec doesn't express (the primary accent scale,
27
+ * containers beyond error, the neutral ladder) keep their
28
+ * mode-resolved base values — the host-announced color MODE
29
+ * (ggui#551) already selects the coherent light/dark ladder these
30
+ * slots resolve from.
31
+ */
32
+ /**
33
+ * Map a host-announced `styles.variables` record onto `--ggui-*`
34
+ * declarations ready for the renderer's scoped token block.
35
+ *
36
+ * Unknown keys and unsafe/blank values are dropped silently (the same
37
+ * tolerant posture as the rest of host-context handling — a partial
38
+ * palette is a partial repaint, never an error). Returns `undefined`
39
+ * when nothing survives so callers can spread-skip the option.
40
+ *
41
+ * @public
42
+ */
43
+ export declare function mapHostPaletteToGguiVars(variables: Readonly<Record<string, string | undefined>> | undefined): Readonly<Record<string, string>> | undefined;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Spec→ggui token bridge (ggui#572).
3
+ *
4
+ * MCP Apps hosts announce their palette as spec `--color-*` custom
5
+ * properties in `hostContext.styles.variables`. The canonical
6
+ * `@modelcontextprotocol/ext-apps` helper applies them as inline
7
+ * custom properties on the iframe `<html>` — which is correct spec
8
+ * behavior but repaints nothing ggui renders: generated UI, the
9
+ * design system, and the shell chrome consume exclusively `--ggui-*`
10
+ * tokens, and the renderer's scoped in-body token block shadows
11
+ * root-level inheritance for every token it defines anyway.
12
+ *
13
+ * This module translates the spec vocabulary onto the ggui token
14
+ * ladder so the renderer can merge the host's palette INTO the scoped
15
+ * block — as the fallback layer beneath the app's own theme (#573
16
+ * ruling: the slice's stamped theme wins; the host palette fills the
17
+ * slots no operator repainted).
18
+ *
19
+ * Coverage is deliberately PARTIAL in both directions:
20
+ *
21
+ * - Spec keys with no faithful ggui counterpart (ghost/disabled/
22
+ * inverse variants, ring colors, fonts, radii, shadows) are
23
+ * dropped — they keep applying via the ext-apps inline-DOM path
24
+ * for any non-ggui content, and inventing a mapping would repaint
25
+ * tokens with the WRONG semantic role.
26
+ * - ggui tokens the spec doesn't express (the primary accent scale,
27
+ * containers beyond error, the neutral ladder) keep their
28
+ * mode-resolved base values — the host-announced color MODE
29
+ * (ggui#551) already selects the coherent light/dark ladder these
30
+ * slots resolve from.
31
+ */
32
+ /**
33
+ * Spec `--color-*` key → ggui token, one entry per slot whose semantic
34
+ * role matches 1:1. Semantic status text maps to the ladder's `-500`
35
+ * stop — the "live" stop tone slots and Alert actually read; a flat
36
+ * `--ggui-color-error` target would repaint nothing (no theme emits
37
+ * flat semantic tokens).
38
+ */
39
+ const SPEC_TO_GGUI = {
40
+ '--color-background-primary': '--ggui-color-surface',
41
+ '--color-background-secondary': '--ggui-color-surfaceVariant',
42
+ '--color-background-tertiary': '--ggui-color-container',
43
+ '--color-background-danger': '--ggui-color-errorContainer',
44
+ '--color-text-primary': '--ggui-color-onSurface',
45
+ '--color-text-secondary': '--ggui-color-onSurfaceVariant',
46
+ '--color-text-tertiary': '--ggui-color-neutral-500',
47
+ '--color-text-danger': '--ggui-color-error-500',
48
+ '--color-text-success': '--ggui-color-success-500',
49
+ '--color-text-warning': '--ggui-color-warning-500',
50
+ '--color-text-info': '--ggui-color-info-500',
51
+ '--color-border-primary': '--ggui-color-outline',
52
+ '--color-border-secondary': '--ggui-color-outlineVariant',
53
+ };
54
+ /**
55
+ * Host palette values arrive over postMessage and are string-joined
56
+ * into a `<style>` element by the renderer — unlike the app's own
57
+ * theme overlay they are NOT pre-validated by the wire parser, so
58
+ * this is the sanitization gate. Conservative deny-list: anything
59
+ * that could close the declaration (`;`), the rule (`{`/`}`), or the
60
+ * style element (`<`/`>`), smuggle strings/escapes, comment out the
61
+ * rest of the sheet (`/*`), or trigger a fetch (`url(`) drops the
62
+ * value. Plain color syntax — hex, rgb()/hsl()/oklch()/color-mix(),
63
+ * named colors — passes untouched.
64
+ */
65
+ function isSafeCssValue(value) {
66
+ if (value.length === 0 || value.length > 256)
67
+ return false;
68
+ if (/[;{}<>"'`\\]/.test(value))
69
+ return false;
70
+ if (/\/\*/.test(value))
71
+ return false;
72
+ if (/url\s*\(/i.test(value))
73
+ return false;
74
+ // Control characters (incl. newlines) have no place in a color value.
75
+ // eslint-disable-next-line no-control-regex
76
+ if (/[\u0000-\u001f\u007f]/.test(value))
77
+ return false;
78
+ return true;
79
+ }
80
+ /**
81
+ * Map a host-announced `styles.variables` record onto `--ggui-*`
82
+ * declarations ready for the renderer's scoped token block.
83
+ *
84
+ * Unknown keys and unsafe/blank values are dropped silently (the same
85
+ * tolerant posture as the rest of host-context handling — a partial
86
+ * palette is a partial repaint, never an error). Returns `undefined`
87
+ * when nothing survives so callers can spread-skip the option.
88
+ *
89
+ * @public
90
+ */
91
+ export function mapHostPaletteToGguiVars(variables) {
92
+ if (variables === undefined)
93
+ return undefined;
94
+ const mapped = {};
95
+ let count = 0;
96
+ for (const [specKey, gguiKey] of Object.entries(SPEC_TO_GGUI)) {
97
+ const raw = variables[specKey];
98
+ if (typeof raw !== 'string')
99
+ continue;
100
+ const value = raw.trim();
101
+ if (!isSafeCssValue(value))
102
+ continue;
103
+ mapped[gguiKey] = value;
104
+ count += 1;
105
+ }
106
+ return count > 0 ? mapped : undefined;
107
+ }