@songsid/agend 2.3.0-beta.6 → 2.3.0-beta.8

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 (193) hide show
  1. package/dist/agent-cli-instructions.md +1 -1
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +4 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-file-read.js +68 -8
  7. package/dist/agent-file-read.js.map +1 -1
  8. package/dist/agent-whats-new.js +4 -0
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.js +3 -1
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.d.ts +38 -0
  13. package/dist/authority/authority-state.js +114 -0
  14. package/dist/authority/authority-state.js.map +1 -0
  15. package/dist/authority/session-authorizations.d.ts +43 -0
  16. package/dist/authority/session-authorizations.js +102 -0
  17. package/dist/authority/session-authorizations.js.map +1 -0
  18. package/dist/authority/web-authority.d.ts +31 -11
  19. package/dist/authority/web-authority.js +39 -12
  20. package/dist/authority/web-authority.js.map +1 -1
  21. package/dist/cache-ledger.d.ts +16 -3
  22. package/dist/cache-ledger.js +33 -13
  23. package/dist/cache-ledger.js.map +1 -1
  24. package/dist/cache-service.d.ts +55 -3
  25. package/dist/cache-service.js +152 -16
  26. package/dist/cache-service.js.map +1 -1
  27. package/dist/channel/mcp-tools.js +13 -7
  28. package/dist/channel/mcp-tools.js.map +1 -1
  29. package/dist/classic-channel-manager.d.ts +2 -2
  30. package/dist/classic-channel-manager.js +2 -6
  31. package/dist/classic-channel-manager.js.map +1 -1
  32. package/dist/config-validator.js +10 -4
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.d.ts +2 -0
  35. package/dist/daemon.js +31 -9
  36. package/dist/daemon.js.map +1 -1
  37. package/dist/delivery-outbox.d.ts +8 -0
  38. package/dist/delivery-outbox.js +7 -1
  39. package/dist/delivery-outbox.js.map +1 -1
  40. package/dist/fleet-manager.d.ts +109 -2
  41. package/dist/fleet-manager.js +280 -12
  42. package/dist/fleet-manager.js.map +1 -1
  43. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +2 -1
  44. package/dist/general-knowledge/skills/multi-channel/SKILL.md +2 -1
  45. package/dist/general-knowledge/skills/web-appearance/SKILL.md +89 -30
  46. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +62 -8
  47. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  48. package/dist/human-delivery-receipts.d.ts +14 -0
  49. package/dist/human-delivery-receipts.js +42 -6
  50. package/dist/human-delivery-receipts.js.map +1 -1
  51. package/dist/instance-config-impact.js +1 -0
  52. package/dist/instance-config-impact.js.map +1 -1
  53. package/dist/instance-names.d.ts +4 -0
  54. package/dist/instance-names.js +12 -0
  55. package/dist/instance-names.js.map +1 -0
  56. package/dist/instructions.d.ts +8 -1
  57. package/dist/instructions.js +9 -2
  58. package/dist/instructions.js.map +1 -1
  59. package/dist/locale.js +52 -0
  60. package/dist/locale.js.map +1 -1
  61. package/dist/outbound-handlers.d.ts +6 -0
  62. package/dist/outbound-handlers.js +49 -2
  63. package/dist/outbound-handlers.js.map +1 -1
  64. package/dist/outbound-schemas.d.ts +57 -1
  65. package/dist/outbound-schemas.js +34 -2
  66. package/dist/outbound-schemas.js.map +1 -1
  67. package/dist/public-web-gateway.js +8 -2
  68. package/dist/public-web-gateway.js.map +1 -1
  69. package/dist/rollout-index.d.ts +5 -0
  70. package/dist/rollout-index.js +26 -5
  71. package/dist/rollout-index.js.map +1 -1
  72. package/dist/settings-api.d.ts +6 -0
  73. package/dist/settings-api.js +54 -1
  74. package/dist/settings-api.js.map +1 -1
  75. package/dist/settings-change.js +31 -0
  76. package/dist/settings-change.js.map +1 -1
  77. package/dist/settings-effect.js +10 -1
  78. package/dist/settings-effect.js.map +1 -1
  79. package/dist/tool-permissions.d.ts +0 -7
  80. package/dist/tool-permissions.js +73 -5
  81. package/dist/tool-permissions.js.map +1 -1
  82. package/dist/topic-commands.d.ts +2 -1
  83. package/dist/topic-commands.js +2 -4
  84. package/dist/topic-commands.js.map +1 -1
  85. package/dist/types.d.ts +13 -2
  86. package/dist/ui/app-effects.js +46 -28
  87. package/dist/ui/chat-ambient.js +307 -0
  88. package/dist/ui/chat-effect-posters.js +8 -8
  89. package/dist/ui/chat-effects.js +51 -22
  90. package/dist/ui/chat-strings.js +21 -6
  91. package/dist/ui/chat-thread.js +1 -1
  92. package/dist/ui/fleet-usage.js +401 -0
  93. package/dist/ui/panel-chat.js +22 -9
  94. package/dist/ui/panel-fleet.js +6 -5
  95. package/dist/ui/panel-settings.js +10 -2
  96. package/dist/ui/settings-appearance.js +8 -0
  97. package/dist/ui/settings-mods.js +19 -6
  98. package/dist/ui/settings-owner-effects.js +3 -3
  99. package/dist/ui/settings-strings.js +30 -0
  100. package/dist/ui/settings-web-authority.js +91 -0
  101. package/dist/ui/shared/app-route.js +1 -1
  102. package/dist/ui/shared/app-session.js +2 -0
  103. package/dist/ui/shared/app-shell.js +2 -1
  104. package/dist/ui/shared/app-themes.js +24 -6
  105. package/dist/ui/shared/app.css +95 -0
  106. package/dist/ui/shared/effect-app-runtime.js +68 -19
  107. package/dist/ui/shared/effect-chat-runtime.js +16 -3
  108. package/dist/ui/shared/effect-frame.js +17 -3
  109. package/dist/ui/shared/effect-policy.js +18 -5
  110. package/dist/ui/shared/effect-profiler.js +31 -9
  111. package/dist/ui/shared/effect-registry.js +17 -41
  112. package/dist/ui/shared/effect-stock-trust.js +4 -4
  113. package/dist/ui/shared/effect-trial-stage.js +18 -7
  114. package/dist/ui/shared/fx-motion.js +67 -0
  115. package/dist/ui/shared/usage-dialog.js +8 -0
  116. package/dist/ui/shared/view-strings.js +4 -0
  117. package/dist/ui/shared/web-authorize.js +151 -0
  118. package/dist/usage-report.d.ts +158 -0
  119. package/dist/usage-report.js +226 -0
  120. package/dist/usage-report.js.map +1 -0
  121. package/dist/usage-rollup.d.ts +51 -0
  122. package/dist/usage-rollup.js +168 -0
  123. package/dist/usage-rollup.js.map +1 -0
  124. package/dist/web-ambient.d.ts +72 -0
  125. package/dist/web-ambient.js +154 -0
  126. package/dist/web-ambient.js.map +1 -0
  127. package/dist/web-api.d.ts +33 -0
  128. package/dist/web-api.js +148 -18
  129. package/dist/web-api.js.map +1 -1
  130. package/dist/web-appearance-tools.d.ts +3 -0
  131. package/dist/web-appearance-tools.js +18 -3
  132. package/dist/web-appearance-tools.js.map +1 -1
  133. package/dist/web-appearance-view.d.ts +14 -2
  134. package/dist/web-appearance-view.js +32 -5
  135. package/dist/web-appearance-view.js.map +1 -1
  136. package/dist/web-appearance.d.ts +15 -1
  137. package/dist/web-appearance.js +77 -1
  138. package/dist/web-appearance.js.map +1 -1
  139. package/dist/web-asset-parts.d.ts +2 -2
  140. package/dist/web-asset-parts.js +3 -3
  141. package/dist/web-asset-parts.js.map +1 -1
  142. package/dist/web-auth.js +3 -1
  143. package/dist/web-auth.js.map +1 -1
  144. package/dist/web-effect-bootstrap.d.ts +12 -0
  145. package/dist/web-effect-bootstrap.js +49 -0
  146. package/dist/web-effect-bootstrap.js.map +1 -0
  147. package/dist/web-effect-bundle.js +46 -1
  148. package/dist/web-effect-bundle.js.map +1 -1
  149. package/dist/web-effect-playback.d.ts +30 -0
  150. package/dist/web-effect-playback.js +71 -0
  151. package/dist/web-effect-playback.js.map +1 -0
  152. package/dist/web-effect-profiles.d.ts +4 -4
  153. package/dist/web-effect-service.d.ts +11 -4
  154. package/dist/web-effect-service.js +25 -4
  155. package/dist/web-effect-service.js.map +1 -1
  156. package/dist/web-effect-tools.d.ts +1 -0
  157. package/dist/web-effect-tools.js +2 -1
  158. package/dist/web-effect-tools.js.map +1 -1
  159. package/dist/web-effect-worker.d.ts +1 -1
  160. package/dist/web-effect-worker.js +2 -2
  161. package/dist/web-effect-worker.js.map +1 -1
  162. package/dist/web-guest-access.js +3 -1
  163. package/dist/web-guest-access.js.map +1 -1
  164. package/dist/web-mod-agent.js +5 -1
  165. package/dist/web-mod-agent.js.map +1 -1
  166. package/dist/web-mod-bundle.d.ts +9 -1
  167. package/dist/web-mod-bundle.js +62 -3
  168. package/dist/web-mod-bundle.js.map +1 -1
  169. package/dist/web-mod-export.d.ts +9 -0
  170. package/dist/web-mod-export.js +21 -0
  171. package/dist/web-mod-export.js.map +1 -1
  172. package/dist/web-mod-store.d.ts +2 -1
  173. package/dist/web-mod-store.js +13 -6
  174. package/dist/web-mod-store.js.map +1 -1
  175. package/dist/web-session-authorization.d.ts +132 -0
  176. package/dist/web-session-authorization.js +193 -0
  177. package/dist/web-session-authorization.js.map +1 -0
  178. package/dist/web-session.d.ts +5 -0
  179. package/dist/web-session.js +16 -0
  180. package/dist/web-session.js.map +1 -1
  181. package/dist/web-settings-patch.d.ts +26 -2
  182. package/dist/web-settings-patch.js +50 -1
  183. package/dist/web-settings-patch.js.map +1 -1
  184. package/dist/web-settings-tools.d.ts +8 -0
  185. package/dist/web-settings-tools.js +14 -1
  186. package/dist/web-settings-tools.js.map +1 -1
  187. package/dist/web-shell-routes.d.ts +1 -1
  188. package/dist/web-shell-routes.js +1 -1
  189. package/dist/web-shell-routes.js.map +1 -1
  190. package/dist/web-theme-store.d.ts +39 -0
  191. package/dist/web-theme-store.js +122 -11
  192. package/dist/web-theme-store.js.map +1 -1
  193. package/package.json +1 -1
@@ -0,0 +1,72 @@
1
+ export declare const AMBIENT_PRESETS: readonly ["fall", "rise", "twinkle", "burst-on-click"];
2
+ export type AmbientPreset = typeof AMBIENT_PRESETS[number];
3
+ /** Where a preset is kept: a chat has one of each. */
4
+ export declare const AMBIENT_KINDS: readonly ["loop", "click"];
5
+ export type AmbientKind = typeof AMBIENT_KINDS[number];
6
+ export declare const ambientKind: (preset: AmbientPreset) => AmbientKind;
7
+ export declare const AMBIENT_GLYPHS_MAX = 4;
8
+ export declare const AMBIENT_GLYPH_GRAPHEMES = 2;
9
+ export declare const AMBIENT_COLORS_MAX = 5;
10
+ export declare const AMBIENT_SPEC_MAX_BYTES = 1024;
11
+ /** The page's caps (chat-ambient.js keeps the same numbers): 24 looping particles, 3 bursts of 16 at once. */
12
+ export declare const AMBIENT_LOOP_MAX = 24;
13
+ export declare const AMBIENT_BURST_MAX = 16;
14
+ export declare const AMBIENT_BURSTS_LIVE = 3;
15
+ type Range = readonly [number, number];
16
+ /** Each preset's bounds and defaults. A count is particles (loops) or particles per click (burst). */
17
+ export declare const AMBIENT_RULES: Record<AmbientPreset, {
18
+ count: Range;
19
+ countDefault: number;
20
+ duration: Range;
21
+ durationDefault: Range;
22
+ }>;
23
+ export declare const AMBIENT_SIZE: Range;
24
+ export declare const AMBIENT_SIZE_DEFAULT: Range;
25
+ export declare const AMBIENT_SWAY_MAX = 80;
26
+ export declare const AMBIENT_SWAY_DEFAULT = 40;
27
+ export declare const AMBIENT_SPREAD: Range;
28
+ export declare const AMBIENT_SPREAD_DEFAULT: Range;
29
+ export declare const AMBIENT_ROTATE_MAX = 720;
30
+ export declare const AMBIENT_ROTATE_DEFAULT = 300;
31
+ export declare const AMBIENT_OPACITY_DEFAULT: readonly [0, 0.85, 0.15];
32
+ export type AmbientGlyph = string | {
33
+ asset: string;
34
+ };
35
+ /** Canonical: every field present, so the page and a preview draw the same thing. */
36
+ export interface AmbientSpec {
37
+ preset: AmbientPreset;
38
+ glyphs: AmbientGlyph[];
39
+ /** Empty: the theme's accent. */
40
+ colors: string[];
41
+ count: number;
42
+ duration: [number, number];
43
+ size: [number, number];
44
+ sway: number;
45
+ spread: [number, number];
46
+ rotate: number;
47
+ opacity: [number, number, number];
48
+ layer: "behind";
49
+ }
50
+ export interface AmbientProblem {
51
+ path: string;
52
+ problem: string;
53
+ }
54
+ export type Checked<T> = {
55
+ ok: true;
56
+ value: T;
57
+ } | {
58
+ ok: false;
59
+ errors: AmbientProblem[];
60
+ };
61
+ /**
62
+ * A spec, closed and canonical: unknown keys, a value out of its preset's bounds or a glyph that is not 1..2 visible
63
+ * graphemes (or one of this instance's `glyph` pictures, when `glyphAsset` is given) are refused.
64
+ */
65
+ export declare function checkAmbient(raw: unknown, opts?: {
66
+ glyphAsset?: (id: string) => boolean;
67
+ }): Checked<AmbientSpec>;
68
+ /** The glyph pictures a spec names. */
69
+ export declare function ambientAssets(spec: AmbientSpec): string[];
70
+ /** One line per problem, for a tool's refusal. */
71
+ export declare function ambientProblems(errors: AmbientProblem[]): string;
72
+ export {};
@@ -0,0 +1,154 @@
1
+ /**
2
+ * #2117 (from #2115's proposal): an **ambient** effect is declarative, like a widget. The agent supplies a small closed
3
+ * spec (a preset, its glyphs and a few numbers); the page draws it with its own code (src/ui/chat-ambient.js): plain
4
+ * nodes animated by the Web Animations API, under the chat's content and never taking a click. No agent code runs, so
5
+ * an ambient needs no Worker, preview origin, HTML-preview consent or FPS baseline (Canvas Worker effects do).
6
+ *
7
+ * A chat holds at most one looping ambient (`fall`, `rise`, `twinkle`) and one click ambient (`burst-on-click`): falling
8
+ * petals and click stars together. One module validates and canonicalises the spec; the store keeps only what this
9
+ * returns, and the page checks the bounds again.
10
+ */
11
+ import { canonicalColor, canonicalText } from "./web-theme.js";
12
+ export const AMBIENT_PRESETS = ["fall", "rise", "twinkle", "burst-on-click"];
13
+ /** Where a preset is kept: a chat has one of each. */
14
+ export const AMBIENT_KINDS = ["loop", "click"];
15
+ export const ambientKind = (preset) => preset === "burst-on-click" ? "click" : "loop";
16
+ export const AMBIENT_GLYPHS_MAX = 4;
17
+ export const AMBIENT_GLYPH_GRAPHEMES = 2;
18
+ export const AMBIENT_COLORS_MAX = 5;
19
+ export const AMBIENT_SPEC_MAX_BYTES = 1024;
20
+ /** The page's caps (chat-ambient.js keeps the same numbers): 24 looping particles, 3 bursts of 16 at once. */
21
+ export const AMBIENT_LOOP_MAX = 24;
22
+ export const AMBIENT_BURST_MAX = 16;
23
+ export const AMBIENT_BURSTS_LIVE = 3;
24
+ /** Each preset's bounds and defaults. A count is particles (loops) or particles per click (burst). */
25
+ export const AMBIENT_RULES = {
26
+ fall: { count: [1, AMBIENT_LOOP_MAX], countDefault: 12, duration: [4, 30], durationDefault: [9, 19] },
27
+ rise: { count: [1, AMBIENT_LOOP_MAX], countDefault: 12, duration: [4, 30], durationDefault: [9, 19] },
28
+ twinkle: { count: [1, AMBIENT_LOOP_MAX], countDefault: 12, duration: [1, 10], durationDefault: [2, 5] },
29
+ "burst-on-click": { count: [4, AMBIENT_BURST_MAX], countDefault: 10, duration: [0.3, 2], durationDefault: [0.5, 0.9] },
30
+ };
31
+ export const AMBIENT_SIZE = [10, 40];
32
+ export const AMBIENT_SIZE_DEFAULT = [14, 22];
33
+ export const AMBIENT_SWAY_MAX = 80;
34
+ export const AMBIENT_SWAY_DEFAULT = 40;
35
+ export const AMBIENT_SPREAD = [20, 120];
36
+ export const AMBIENT_SPREAD_DEFAULT = [34, 90];
37
+ export const AMBIENT_ROTATE_MAX = 720;
38
+ export const AMBIENT_ROTATE_DEFAULT = 300;
39
+ export const AMBIENT_OPACITY_DEFAULT = [0, 0.85, 0.15];
40
+ const KEYS = ["preset", "glyphs", "colors", "count", "duration", "size", "sway", "spread", "rotate", "opacity", "layer"];
41
+ const ASSET_ID = /^[0-9a-f]{32}$/;
42
+ const isObj = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.getPrototypeOf(v) === Object.prototype;
43
+ const isNum = (v) => typeof v === "number" && Number.isFinite(v);
44
+ /** Two decimals are plenty for seconds, pixels and opacity, and keep the stored spec short. */
45
+ const round = (n) => Math.round(n * 100) / 100;
46
+ /**
47
+ * A spec, closed and canonical: unknown keys, a value out of its preset's bounds or a glyph that is not 1..2 visible
48
+ * graphemes (or one of this instance's `glyph` pictures, when `glyphAsset` is given) are refused.
49
+ */
50
+ export function checkAmbient(raw, opts = {}) {
51
+ const errors = [];
52
+ const bad = (path, problem) => { errors.push({ path, problem }); };
53
+ if (!isObj(raw))
54
+ return { ok: false, errors: [{ path: "ambient", problem: "an object { preset, glyphs, … }" }] };
55
+ for (const k of Object.keys(raw))
56
+ if (!KEYS.includes(k))
57
+ bad(k.slice(0, 40), "unknown key");
58
+ const preset = raw.preset;
59
+ if (!AMBIENT_PRESETS.includes(preset))
60
+ return { ok: false, errors: [...errors, { path: "preset", problem: AMBIENT_PRESETS.join(" | ") }] };
61
+ const rules = AMBIENT_RULES[preset];
62
+ const glyphs = [];
63
+ if (!Array.isArray(raw.glyphs) || raw.glyphs.length < 1 || raw.glyphs.length > AMBIENT_GLYPHS_MAX)
64
+ bad("glyphs", `1..${AMBIENT_GLYPHS_MAX} glyphs`);
65
+ else
66
+ raw.glyphs.forEach((g, i) => {
67
+ if (typeof g === "string") {
68
+ const t = canonicalText(g, 1, AMBIENT_GLYPH_GRAPHEMES);
69
+ if (t === null)
70
+ bad(`glyphs.${i}`, `1..${AMBIENT_GLYPH_GRAPHEMES} visible characters (an emoji, ✦, ❀)`);
71
+ else
72
+ glyphs.push(t);
73
+ }
74
+ else if (isObj(g) && Object.keys(g).length === 1 && typeof g.asset === "string" && ASSET_ID.test(g.asset)) {
75
+ if (opts.glyphAsset && !opts.glyphAsset(g.asset))
76
+ bad(`glyphs.${i}.asset`, "not one of this instance's glyph pictures (set_web_theme_asset slot glyph)");
77
+ else
78
+ glyphs.push({ asset: g.asset });
79
+ }
80
+ else
81
+ bad(`glyphs.${i}`, "a short string, or { asset: <id> }");
82
+ });
83
+ const colors = [];
84
+ if (raw.colors !== undefined) {
85
+ if (!Array.isArray(raw.colors) || raw.colors.length > AMBIENT_COLORS_MAX)
86
+ bad("colors", `0..${AMBIENT_COLORS_MAX} colours`);
87
+ else
88
+ raw.colors.forEach((c, i) => { const v = canonicalColor(c); if (v === null)
89
+ bad(`colors.${i}`, "#rgb or #rrggbb");
90
+ else
91
+ colors.push(v); });
92
+ }
93
+ const int = (key, [lo, hi], dflt) => {
94
+ const v = raw[key];
95
+ if (v === undefined)
96
+ return dflt;
97
+ if (!Number.isSafeInteger(v) || v < lo || v > hi) {
98
+ bad(key, `a whole number ${lo}..${hi}${key === "count" ? ` for ${preset}` : ""}`);
99
+ return dflt;
100
+ }
101
+ return v;
102
+ };
103
+ const num = (key, [lo, hi], dflt) => {
104
+ const v = raw[key];
105
+ if (v === undefined)
106
+ return dflt;
107
+ if (!isNum(v) || v < lo || v > hi) {
108
+ bad(key, `a number ${lo}..${hi}`);
109
+ return dflt;
110
+ }
111
+ return round(v);
112
+ };
113
+ const range = (key, [lo, hi], dflt) => {
114
+ const v = raw[key];
115
+ if (v === undefined)
116
+ return [dflt[0], dflt[1]];
117
+ if (!Array.isArray(v) || v.length !== 2 || !v.every(isNum) || v[0] < lo || v[1] > hi || v[0] > v[1]) {
118
+ bad(key, `[min, max] within ${lo}..${hi}${key === "duration" ? ` seconds for ${preset}` : ""}`);
119
+ return [dflt[0], dflt[1]];
120
+ }
121
+ return [round(v[0]), round(v[1])];
122
+ };
123
+ const count = int("count", rules.count, rules.countDefault);
124
+ const duration = range("duration", rules.duration, rules.durationDefault);
125
+ const size = range("size", AMBIENT_SIZE, AMBIENT_SIZE_DEFAULT);
126
+ const sway = num("sway", [0, AMBIENT_SWAY_MAX], AMBIENT_SWAY_DEFAULT);
127
+ const spread = range("spread", AMBIENT_SPREAD, AMBIENT_SPREAD_DEFAULT);
128
+ const rotate = num("rotate", [0, AMBIENT_ROTATE_MAX], AMBIENT_ROTATE_DEFAULT);
129
+ let opacity = [...AMBIENT_OPACITY_DEFAULT];
130
+ if (raw.opacity !== undefined) {
131
+ const o = raw.opacity;
132
+ if (!Array.isArray(o) || o.length !== 3 || !o.every(x => isNum(x) && x >= 0 && x <= 1))
133
+ bad("opacity", "[start, peak, end], each 0..1");
134
+ else
135
+ opacity = [round(o[0]), round(o[1]), round(o[2])];
136
+ }
137
+ if (raw.layer !== undefined && raw.layer !== "behind")
138
+ bad("layer", "\"behind\" (an ambient never covers the chat)");
139
+ if (errors.length)
140
+ return { ok: false, errors };
141
+ const spec = { preset, glyphs, colors, count, duration, size, sway, spread, rotate, opacity, layer: "behind" };
142
+ if (Buffer.byteLength(JSON.stringify(spec), "utf8") > AMBIENT_SPEC_MAX_BYTES)
143
+ return { ok: false, errors: [{ path: "ambient", problem: `at most ${AMBIENT_SPEC_MAX_BYTES} bytes` }] };
144
+ return { ok: true, value: spec };
145
+ }
146
+ /** The glyph pictures a spec names. */
147
+ export function ambientAssets(spec) {
148
+ return spec.glyphs.flatMap(g => typeof g === "string" ? [] : [g.asset]);
149
+ }
150
+ /** One line per problem, for a tool's refusal. */
151
+ export function ambientProblems(errors) {
152
+ return errors.map(e => `${e.path}: ${e.problem}`).join("; ");
153
+ }
154
+ //# sourceMappingURL=web-ambient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"web-ambient.js","sourceRoot":"","sources":["../src/web-ambient.ts"],"names":[],"mappings":"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"}
package/dist/web-api.d.ts CHANGED
@@ -49,9 +49,11 @@ export interface WebApiContext {
49
49
  group_id?: number | string;
50
50
  mode?: string;
51
51
  };
52
+ health_port?: number;
52
53
  web?: {
53
54
  echo_to_channel?: boolean;
54
55
  usage_panel?: boolean;
56
+ usage_report_on_public_link?: boolean;
55
57
  upload_max_file_mb?: number;
56
58
  upload_max_total_mb?: number;
57
59
  };
@@ -130,6 +132,7 @@ export interface WebApiContext {
130
132
  /** Current owned pane reader shared with status; absent means context is unavailable. */
131
133
  getPaneContextSource?(name: string): PaneContextSource | null;
132
134
  /** #1306: which preview origin this /ui load may frame (Host and the TLS signal only); absent: previews off. */
135
+ readonly effectBoot?: string;
133
136
  previewForUi?(hostHeader: string | undefined, secure: boolean): PreviewAvailability & {
134
137
  boot: string | null;
135
138
  };
@@ -196,6 +199,11 @@ export interface WebApiContext {
196
199
  logSteps?(name: string, q: import("./log-service.js").LogQuery): Promise<unknown>;
197
200
  /** #1468: the prompt-cache expiry analysis for a window ("24h" | "7d" | "30d"); absent: not offered. */
198
201
  cacheReport?(window: string): Promise<unknown>;
202
+ /** #2122: the usage report (Fleet → Usage); absent: not offered. */
203
+ usageReport?(params: {
204
+ range: import("./usage-report.js").UsageRange;
205
+ group: import("./usage-report.js").UsageGroup;
206
+ }): Promise<unknown>;
199
207
  /** #1269: an instance's chat command from the web chat (web-commands.ts); absent: not offered. */
200
208
  webCommand?(input: {
201
209
  instance: string;
@@ -210,6 +218,29 @@ export interface WebApiContext {
210
218
  status: number;
211
219
  body: unknown;
212
220
  }>;
221
+ /** #1939 S2b: "Authorize this browser" — the Confirm card in the account's DM; and what the app shows for this browser. */
222
+ requestSessionAuthorization?(input: {
223
+ session: SessionRecord | null;
224
+ gateway: boolean;
225
+ peer: WebPeer;
226
+ }): Promise<{
227
+ ok: true;
228
+ expiresInMs: number;
229
+ } | {
230
+ ok: false;
231
+ refusal: string;
232
+ retryAfterMs?: number;
233
+ }>;
234
+ webSessionAuthorization?(input: {
235
+ session: SessionRecord | null;
236
+ gateway: boolean;
237
+ peer: WebPeer;
238
+ }): {
239
+ state: string;
240
+ until?: number;
241
+ expiresInMs?: number;
242
+ reason?: string;
243
+ };
213
244
  /** #1939 S1: the design's §3 for one request (`instance` once the body names it); absent: web carries no authority. */
214
245
  webAuthority?(input: {
215
246
  session: SessionRecord | null;
@@ -350,6 +381,8 @@ export type PublicPreviewForUi = PreviewAvailability & {
350
381
  detail?: string;
351
382
  };
352
383
  export interface AppShellContext {
384
+ /** #2118: independent trusted opaque effect document, never HTML-preview consent or listener state. */
385
+ readonly effectBoot?: string;
353
386
  previewForUi?(hostHeader: string | undefined, secure: boolean): (PreviewAvailability & {
354
387
  boot: string | null;
355
388
  }) | null;
package/dist/web-api.js CHANGED
@@ -516,7 +516,12 @@ function handleEffects(req, res, url, ctx, path, method) {
516
516
  const reading = method === "GET";
517
517
  // Reports are bounded untrusted diagnostics, not an appearance or admission write (r2 §6).
518
518
  const profilePost = method === "POST" && /^\/ui\/effects\/profile\/[0-9a-f]{32}$/.test(path);
519
- if (gatewayRequestContext(req)) {
519
+ const gateway = gatewayRequestContext(req), guest = guestRequestOf(req);
520
+ const instanceQuery = url.searchParams.get("instance"), keys = [...url.searchParams.keys()];
521
+ const chatRead = reading && (path === "/ui/effects/placements" || /^\/ui\/effects\/resource\/[0-9a-f]{32}$/.test(path))
522
+ && keys.length === 2 && keys.includes("instance") && keys.includes("ownerEpoch") && !!instanceQuery && instanceQuery.length <= 128;
523
+ // Public viewing grants current Chat reads, never ordinary library/owner/Trial access or writes.
524
+ if (gateway && (!chatRead || !gateway.isCurrent())) {
520
525
  json(res, reading ? 404 : 403, { error: "effects_not_over_public_link" });
521
526
  return true;
522
527
  }
@@ -528,7 +533,9 @@ function handleEffects(req, res, url, ctx, path, method) {
528
533
  return { status: 403, error: "effects_need_a_signed_in_person" };
529
534
  if (!reading && !profilePost && verdict.session.tier !== "admin")
530
535
  return { status: 403, error: "effects_read_only" };
531
- return { person: webPersonKey(verdict.session.owner), canWrite: verdict.session.tier === "admin", epoch: verdict.session.handle };
536
+ if (verdict.session.tier === "guest" && (!chatRead || !guest || !guest.current() || !guest.scope().has(instanceQuery)))
537
+ return { status: 404, error: "not_found" };
538
+ return { person: webPersonKey(verdict.session.owner), canWrite: verdict.session.tier === "admin", epoch: verdict.session.handle, guest: verdict.session.tier === "guest" };
532
539
  };
533
540
  const first = who();
534
541
  if ("status" in first) {
@@ -571,9 +578,9 @@ function handleEffects(req, res, url, ctx, path, method) {
571
578
  else if (path === "/ui/effects/owner" && instance === undefined && slot === null)
572
579
  answer(service.ownerSelections(first.person));
573
580
  else if (path === "/ui/effects/placements" && instance !== undefined)
574
- answer(service.placements(first.person, instance));
581
+ answer(service.placements(first.person, instance, !!gateway || first.guest));
575
582
  else if (idPath?.[1] === "resource")
576
- answer(slot === null ? service.resource(first.person, idPath[2], instance) : service.ownerResource(first.person, idPath[2], slot));
583
+ answer(slot === null ? service.resource(first.person, idPath[2], instance, !!gateway || first.guest) : service.ownerResource(first.person, idPath[2], slot));
577
584
  else if (idPath?.[1] === "profile" && instance === undefined)
578
585
  answer(service.profile(first.person, idPath[2]));
579
586
  else
@@ -627,8 +634,13 @@ function handleEffects(req, res, url, ctx, path, method) {
627
634
  answer(service.remove(now.person, body));
628
635
  else if (path === "/ui/effects/share")
629
636
  answer(service.share(now.person, body));
630
- else
631
- answer(service.report(now.person, idPath[2], body, instance));
637
+ else {
638
+ if (body && typeof body === "object" && body.kind === "playback" && (url.searchParams.getAll("instance").length !== 1 || slot !== null)) {
639
+ json(res, 400, { error: "playback_report" });
640
+ return;
641
+ }
642
+ answer(service.report(now.person, idPath[2], body, instance, now.epoch));
643
+ }
632
644
  })().catch(() => { try {
633
645
  json(res, 500, { error: "effect_request_failed" });
634
646
  }
@@ -837,7 +849,7 @@ export function exportMod(ctx, person, body) {
837
849
  return { ok: false, status: 400, error: "title", detail: "1..60 characters on one line" };
838
850
  const parts = {};
839
851
  let omitted = 0;
840
- const wantsInstance = inc.theme === true || inc.identity === true || (isObj(inc.widgets) && AGENT_WIDGET_SLOTS.some(s => inc.widgets[s] === true));
852
+ const wantsInstance = inc.theme === true || inc.identity === true || inc.ambient === true || (isObj(inc.widgets) && AGENT_WIDGET_SLOTS.some(s => inc.widgets[s] === true));
841
853
  let name = null;
842
854
  if (wantsInstance) {
843
855
  if (typeof body.instance !== "string" || !ctx.isKnownInstance?.(body.instance) || !ctx.themeStore)
@@ -864,6 +876,11 @@ export function exportMod(ctx, person, body) {
864
876
  if (isObj(inc.widgets) && inc.widgets[slot] === true && w)
865
877
  (parts.widgets ??= {})[slot] = { spec: w.spec, data: w.data };
866
878
  }
879
+ // #2117: its ambients, whoever set them (a person exports what is on the chat).
880
+ if (inc.ambient === true) {
881
+ const specs = Object.fromEntries(Object.entries(rec.placements.ambient).flatMap(([k, a]) => a ? [[k, a.spec]] : []));
882
+ parts.ambient = { specs, picture: id => { const a = rec.assets.find(x => x.id === id); const b = store.asset(name, id); return a && b ? { bytes: b.bytes, kind: a.kind } : null; } };
883
+ }
867
884
  }
868
885
  if (isObj(inc.widgets) && inc.widgets.sidebar === true && ctx.webPrefs) {
869
886
  const v = ctx.webPrefs.read(OPERATOR).values["widget.sidebar"];
@@ -914,6 +931,19 @@ function webAuthorityOf(req, url, ctx, instance) {
914
931
  return ctx.webAuthority({ session: v.kind === "allow" && v.via === "session" ? v.session : null, gateway: !!gatewayRequestContext(req),
915
932
  peer: classifyWebPeer(req), ...(instance !== undefined ? { instance } : {}) });
916
933
  }
934
+ /**
935
+ * #2122: what let this request in, asked now — its session (by handle, under the token it was signed in with) or the
936
+ * header token; null when nothing does any more (the gate also refuses a public link that is no longer the current
937
+ * one). Equal before and after an await: the same admission still holds. Never touches the session.
938
+ */
939
+ function admissionOf(req, url, ctx) {
940
+ const gateway = gatewayRequestContext(req);
941
+ const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
942
+ if (v.kind !== "allow")
943
+ return null;
944
+ const via = v.via === "session" ? `session:${v.session.handle}:${v.session.tokenEpoch}` : "token";
945
+ return gateway ? `${via}@${gateway.exposureId}` : via;
946
+ }
917
947
  /** #1827: the person, or the request is answered 401 here (it is no longer authorized) and null is returned. */
918
948
  function webPersonOr401(req, res, url, ctx) {
919
949
  const p = webPersonOf(req, url, ctx);
@@ -1282,9 +1312,10 @@ export function handleWebRequest(req, res, url, ctx) {
1282
1312
  return true;
1283
1313
  }
1284
1314
  // ── #1629: agents' themes — read by every page (the public link included: it views them), changed by a person ──
1285
- // GET /ui/themes: { rev, themes, locked, widgets } when the status frame's themes_rev moves (passive, web-auth.ts).
1315
+ // GET /ui/themes: { rev, themes, locked, widgets, ambient (#2117) } when the status frame's themes_rev moves (passive, web-auth.ts).
1286
1316
  // GET /ui/img/theme/<instance>/<id>: a theme's picture (same headers as #1630's images; the id is the content hash).
1287
- // POST /ui/themes/<instance>/reset | lock | widget (r4-4: { slot } — take an agent's widget out): a person's, from an admin session — never the CLI token, never the public
1317
+ // POST /ui/themes/<instance>/reset | lock | widget (r4-4: { slot } — take an agent's widget out) | ambient (#2117: { spec } sets one,
1318
+ // { which? } alone clears that kind or both): a person's, from an admin session — never the CLI token, never the public
1288
1319
  // link (absent from its manifest, and refused here too).
1289
1320
  if (method === "GET" && path === "/ui/themes") {
1290
1321
  const store = ctx.themeStore;
@@ -1292,7 +1323,7 @@ export function handleWebRequest(req, res, url, ctx) {
1292
1323
  json(res, 404, { error: "Themes are not kept on this fleet" });
1293
1324
  return true;
1294
1325
  }
1295
- json(res, 200, { rev: store.rev(), themes: store.themes(), locked: store.lockedNames(), widgets: store.widgets() });
1326
+ json(res, 200, { rev: store.rev(), themes: store.themes(), locked: store.lockedNames(), widgets: store.widgets(), ambient: store.ambients() });
1296
1327
  return true;
1297
1328
  }
1298
1329
  const themeImage = path.match(/^\/ui\/img\/theme\/([^/]+)\/([0-9a-f]{32})$/);
@@ -1318,7 +1349,7 @@ export function handleWebRequest(req, res, url, ctx) {
1318
1349
  res.end(image.bytes);
1319
1350
  return true;
1320
1351
  }
1321
- const themeWrite = path.match(/^\/ui\/themes\/([^/]+)\/(reset|lock|widget)$/);
1352
+ const themeWrite = path.match(/^\/ui\/themes\/([^/]+)\/(reset|lock|widget|ambient)$/);
1322
1353
  if (method === "POST" && themeWrite) {
1323
1354
  const service = ctx.webAppearance;
1324
1355
  if (!service) {
@@ -1370,7 +1401,8 @@ export function handleWebRequest(req, res, url, ctx) {
1370
1401
  }
1371
1402
  const r = themeWrite[2] === "reset" ? service.humanReset(name)
1372
1403
  : themeWrite[2] === "widget" ? service.humanRemoveWidget(name, body.slot)
1373
- : typeof body.locked === "boolean" ? service.humanLock(name, body.locked) : { error: "locked: true or false", status: 400 };
1404
+ : themeWrite[2] === "ambient" ? service.humanSetAmbient(name, body.spec ?? null, body.which)
1405
+ : typeof body.locked === "boolean" ? service.humanLock(name, body.locked) : { error: "locked: true or false", status: 400 };
1374
1406
  if (typeof r.error === "string") {
1375
1407
  json(res, typeof r.status === "number" ? r.status : 503, { error: r.error });
1376
1408
  return;
@@ -1771,6 +1803,50 @@ export function handleWebRequest(req, res, url, ctx) {
1771
1803
  // #1269: an instance's chat command (/ctx, /compact, /clear, /model, …) — a session write (CSRF, checked by the
1772
1804
  // gate), run through the platforms' own handlers and command table. The public link is told it is one (/save is
1773
1805
  // refused there).
1806
+ // ── #1939 S2b: this browser's authorization — what the app shows (GET), and "Authorize this browser" (POST: posts the
1807
+ // Confirm card in the account's DM). Judged on this request (§3), never touching the session.
1808
+ if (path === "/ui/authorization" && (method === "GET" || method === "POST")) {
1809
+ const judge = () => {
1810
+ const v = evaluateWebRequest(req, url, ctx.webToken ?? null, ctx.webSessions, { touch: false });
1811
+ return { session: v.kind === "allow" && v.via === "session" ? v.session : null, gateway: !!gatewayRequestContext(req), peer: classifyWebPeer(req) };
1812
+ };
1813
+ res.setHeader("Cache-Control", "no-store");
1814
+ if (method === "GET") {
1815
+ if (!ctx.webSessionAuthorization) {
1816
+ json(res, 404, { error: "not available" });
1817
+ return true;
1818
+ }
1819
+ json(res, 200, ctx.webSessionAuthorization(judge()));
1820
+ return true;
1821
+ }
1822
+ if (!ctx.requestSessionAuthorization) {
1823
+ json(res, 404, { error: "not available" });
1824
+ return true;
1825
+ }
1826
+ const ask = ctx.requestSessionAuthorization.bind(ctx);
1827
+ (async () => {
1828
+ try {
1829
+ await parseBody(req);
1830
+ }
1831
+ catch {
1832
+ json(res, 400, { error: "Invalid JSON" });
1833
+ return;
1834
+ }
1835
+ if (!permitWebContinuation(req, res, ctx))
1836
+ return;
1837
+ const r = await ask(judge()); // asked again now the body is in
1838
+ // A refusal is an answer the page shows (pending, three this hour, why not), not a failed request: 200 with
1839
+ // ok: false — a 4xx would be logged as an error in the browser's console every time.
1840
+ json(res, 200, r);
1841
+ })().catch(err => {
1842
+ ctx.logger.error({ err: err.message }, "Web authorization request failed");
1843
+ try {
1844
+ json(res, 500, { error: "Request failed" });
1845
+ }
1846
+ catch { /* already answered */ }
1847
+ });
1848
+ return true;
1849
+ }
1774
1850
  if (method === "POST" && path === "/ui/command") {
1775
1851
  if (!ctx.webCommand) {
1776
1852
  json(res, 404, { error: "Not available" });
@@ -2252,6 +2328,47 @@ export function handleWebRequest(req, res, url, ctx) {
2252
2328
  }
2253
2329
  return true;
2254
2330
  }
2331
+ // #2122: the usage report. Signed-in sessions by default (guests never: not in GUEST_ROUTES); hidden with the rest of
2332
+ // usage by web.usage_panel: false; over the public link only when web.usage_report_on_public_link is true. Answered
2333
+ // from the ledgers and rollups as they are; a catch-up runs behind. The page's CSV is built from this same answer.
2334
+ if (method === "GET" && path === "/ui/usage-report") {
2335
+ // Who may have it, asked on arrival and again before it is sent (the report is awaited: a switch turned off, a
2336
+ // session signed out or a public link ended meanwhile gets nothing). The same boundary both times, no new gate.
2337
+ const refusal = () => {
2338
+ if (!ctx.usageReport || ctx.fleetConfig?.web?.usage_panel === false)
2339
+ return [404, "Not available"];
2340
+ if (gatewayRequestContext(req) && ctx.fleetConfig?.web?.usage_report_on_public_link !== true)
2341
+ return [403, "usage_report_not_over_public_link"];
2342
+ return null;
2343
+ };
2344
+ // Over the public link, where to turn it on: Settings → Web on the dashboard reached directly (this computer's own
2345
+ // address — the public link's Settings only reads). Never the public origin's own Settings, which cannot change it.
2346
+ const answer = ([status, error]) => json(res, status, {
2347
+ error, ...(status === 403 ? { settings: `http://localhost:${ctx.fleetConfig?.health_port ?? 19280}/settings/web` } : {})
2348
+ });
2349
+ const first = refusal();
2350
+ if (first) {
2351
+ answer(first);
2352
+ return true;
2353
+ }
2354
+ const admitted = admissionOf(req, url, ctx);
2355
+ if (!admitted) {
2356
+ json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2357
+ return true;
2358
+ }
2359
+ void import("./usage-report.js").then(({ usageRange, usageGroup }) => ctx.usageReport({
2360
+ range: usageRange(url.searchParams.get("range"), url.searchParams.get("from") ?? undefined, url.searchParams.get("to") ?? undefined),
2361
+ group: usageGroup(url.searchParams.get("group")),
2362
+ })).then((report) => {
2363
+ const now = refusal();
2364
+ if (now)
2365
+ return answer(now);
2366
+ if (admissionOf(req, url, ctx) !== admitted)
2367
+ return json(res, 401, { error: WEB_TOKEN_INVALID_MESSAGE });
2368
+ json(res, 200, report);
2369
+ }, (err) => { ctx.logger.error({ err }, "usage report failed"); json(res, 500, { error: "Usage report failed" }); });
2370
+ return true;
2371
+ }
2255
2372
  // #1468: the prompt-cache expiry analysis. Opening it is a person's navigation, like any Fleet tab (it counts as
2256
2373
  // use); the page never re-reads it on a timer. Answered from the ledgers as they are; a catch-up runs behind.
2257
2374
  if (method === "GET" && path === "/ui/cache") {
@@ -2873,23 +2990,33 @@ function shellHtmlTag(ctx, mode, viewer) {
2873
2990
  * the public link, the anonymous View reader, the CLI's header token or the sign-in and setup pages (other panels).
2874
2991
  */
2875
2992
  function shellOwnerCss(ctx, mode, viewer) {
2876
- return mode === "full" && viewer?.person !== null && viewer?.person !== undefined && ctx.ownerCss ? ctx.ownerCss.active(viewer.person) : null;
2993
+ return mode === "full" && !viewer?.guest && viewer?.person !== null && viewer?.person !== undefined && ctx.ownerCss ? ctx.ownerCss.active(viewer.person) : null;
2877
2994
  }
2878
2995
  function guestBodyAttrs(ctx, owner, attr) {
2879
2996
  const badge = ctx.guestBadge?.(owner);
2880
2997
  return ` data-tier="guest"${badge ? ` data-guest-name="${attr(badge.name)}" data-guest-platform="${attr(badge.platform)}"` : ""}`;
2881
2998
  }
2999
+ /** A separate session fence for Chat playback: public/guest viewers acquire no owner-pref identity. */
3000
+ function shellEffect(req, ctx, mode) {
3001
+ if (mode !== "full" || typeof ctx.effectBoot !== "string" || !/^[0-9a-f]{32}$/.test(ctx.effectBoot))
3002
+ return null;
3003
+ const v = evaluateWebRequest(req, new URL(req.url ?? "/", "http://localhost"), ctx.webToken ?? null, ctx.webSessions, { touch: false });
3004
+ if (v.kind !== "allow" || v.via !== "session" || !v.session || !/^[a-f0-9]{16}$/.test(v.session.handle))
3005
+ return null;
3006
+ return { boot: ctx.effectBoot, epoch: v.session.handle, scope: gatewayRequestContext(req) || v.session.tier === "guest" ? "chat" : "owner" };
3007
+ }
2882
3008
  function shellBodyTag(req, ctx, mode, viewer) {
2883
3009
  const p = shellPreview(req, ctx, mode);
2884
- const ownerCss = shellOwnerCss(ctx, mode, viewer);
2885
- let ownerCssState = "unknown";
2886
- if (mode === "full" && viewer?.person != null && ctx.ownerCss) {
3010
+ const ownerCss = shellOwnerCss(ctx, mode, viewer), effect = shellEffect(req, ctx, mode);
3011
+ let ownerCssState = mode === "full" && (gatewayRequestContext(req) || viewer?.guest) ? "off" : "unknown";
3012
+ if (mode === "full" && !viewer?.guest && viewer?.person != null && ctx.ownerCss) {
2887
3013
  ownerCssState = ctx.ownerCss.effectState(viewer.person);
2888
3014
  if (ownerCssState === "off" && ownerCss !== null || ownerCssState === "on" && ownerCss === null)
2889
3015
  ownerCssState = "unknown";
2890
3016
  }
2891
3017
  const attr = (v) => String(v ?? "").replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
2892
3018
  return `<body data-mode="${mode}"${mode === "full" && gatewayRequestContext(req) ? ' data-web-transport="poll"' : ""} data-dashboard-origin="${attr(p?.dashboardOrigin)}" data-preview-origin="${attr(p?.previewOrigin)}"`
3019
+ + `${effect ? ` data-effect-boot="${effect.boot}" data-effect-epoch="${effect.epoch}" data-effect-scope="${effect.scope}"` : ""}`
2893
3020
  + ` data-preview-boot="${attr(p?.previewOrigin ? p.boot : "")}" data-preview-reason="${attr(p ? p.reason : "Previews are not available on this fleet.")}"`
2894
3021
  // #1554: no availability asked (the public link, or not the full app) still says why, in the page's language.
2895
3022
  + ` data-preview-reason-code="${attr(p ? p.code ?? "" : gatewayRequestContext(req) ? "publicLink" : "notOffered")}"`
@@ -2925,8 +3052,11 @@ export function serveAppShell(req, res, ctx, mode) {
2925
3052
  const mediaSrc = own ? `${own}/ui/file/` : "'none'";
2926
3053
  // A public-link page frames what its exposure's preview names (the quick-tunnel wildcard: its host is not known when
2927
3054
  // the page may load); every other page frames exactly the origin this load chose (#1723).
2928
- const frameSrc = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin
2929
- ? `${p.previewOrigin}/frame${mode === "full" && !gatewayRequestContext(req) ? ` ${p.previewOrigin}/effect` : ""}` : null;
3055
+ const previewFrames = p && "frameSrc" in p ? p.frameSrc : p?.previewOrigin ? `${p.previewOrigin}/frame` : null;
3056
+ // This fixed trusted document is independent of HTML preview. Its own response confines all Worker execution.
3057
+ const effectOrigin = gatewayRequestContext(req)?.expectedOrigin ?? dashboardOriginFor(typeof req.headers.host === "string" ? req.headers.host : undefined, isSecureRequest(req));
3058
+ const effectFrame = shellEffect(req, ctx, mode) && effectOrigin ? `${effectOrigin}/effect-bootstrap` : null;
3059
+ const frameSrc = [previewFrames, effectFrame].filter(Boolean).join(" ");
2930
3060
  sendPanelHtml(res, html.replace('<html lang="en">', shellHtmlTag(ctx, mode, viewer)).replace("<body>", shellBodyTag(req, ctx, mode, viewer)), 200, {}, { ...(frameSrc ? { frameSrc } : {}), mediaSrc });
2931
3061
  }
2932
3062
  catch {