@hosty-sdk/app 0.24.0 → 0.26.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.
package/dist/server.js CHANGED
@@ -1,7 +1,9 @@
1
1
  // Server slice: Next route-handler factories and Core revalidation. Never reaches a client
2
2
  // bundle — the service token lives here.
3
3
  import "server-only";
4
+ import { readAppContinuity } from "./continuity.js";
4
5
  import { createHostySessionResponse } from "./session-server.js";
6
+ import { createHostyUiResponse } from "./ui-server.js";
5
7
  import { resolveAppAuthProtocol, appAuthProtocolMinimum, isValidCodeVerifier } from "./app-code.js";
6
8
  export { isValidCodeVerifier } from "./app-code.js";
7
9
  import { classifyRevalidationHttpStatus, } from "./index.js";
@@ -28,6 +30,14 @@ export function createHostySessionRouteHandler(config, options = {}) {
28
30
  return createHostySessionResponse(session, recovery, { ...options, signal: request.signal });
29
31
  };
30
32
  }
33
+ /** Mount GET/POST at /api/hosty/ui/[...action] for the shared panel protocol. */
34
+ export function createHostyUiRouteHandler(config, options = {}) {
35
+ return async (request) => {
36
+ const token = readAppIdentityToken(request.headers, config);
37
+ const session = await resolveAppSession(token, config);
38
+ return createHostyUiResponse(request, { session, token }, { ...options, appId: getAppId(config) });
39
+ };
40
+ }
31
41
  export function getCoreOrigin() {
32
42
  return process.env.HOSTY_CORE_ORIGIN?.trim() || null;
33
43
  }
@@ -212,6 +222,7 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
212
222
  expiresAt,
213
223
  activeUntil: readString(payload.activeUntil),
214
224
  activityRequired: payload.activityRequired === true,
225
+ continuity: readAppContinuity(payload.continuity),
215
226
  };
216
227
  const expiresAtMs = expiresAt ? Date.parse(expiresAt) : Number.NaN;
217
228
  if (Number.isFinite(expiresAtMs) && expiresAtMs > Date.now()) {
@@ -1,5 +1,6 @@
1
1
  import { type ProviderServerOptions } from "@hosty-sdk/app/providers/server";
2
2
  import type { AppSessionStatus, SessionRecoveryParams } from "./session-types.js";
3
+ import type { AppContinuity } from "./continuity.js";
3
4
  /** Framework-neutral server adapter. Call with an already authenticated app session, never request data. */
4
5
  export type HostySessionInput = {
5
6
  status: AppSessionStatus;
@@ -8,6 +9,7 @@ export type HostySessionInput = {
8
9
  hostRole: string | null;
9
10
  activeUntil?: string | null;
10
11
  activityRequired?: boolean;
12
+ continuity?: AppContinuity;
11
13
  };
12
14
  error?: {
13
15
  code: string;
@@ -0,0 +1,2 @@
1
+ import type { UiHost } from "./ui-host.js";
2
+ export declare const UiHostContext: import("react").Context<UiHost | null>;
@@ -0,0 +1,3 @@
1
+ "use client";
2
+ import { createContext } from "react";
3
+ export const UiHostContext = createContext(null);
@@ -0,0 +1,45 @@
1
+ import { type UiClient } from "./ui.js";
2
+ import { type PanelTarget, type PresentationOptions, type PresentationResult, type UiEnvironment, type UiEvent } from "./ui-types.js";
3
+ /** Direct-child transport. Discovery carries no identity and never creates a fallback host. */
4
+ export declare class EmbeddedUiClient implements UiClient {
5
+ private readonly path;
6
+ private environment;
7
+ private nonce;
8
+ private readonly listeners;
9
+ private readonly pending;
10
+ private channel;
11
+ private verifying;
12
+ private disposed;
13
+ private deadline;
14
+ private retries;
15
+ private state;
16
+ private active;
17
+ private offered;
18
+ private readonly onMessage;
19
+ private readonly onFocus;
20
+ private readonly onKey;
21
+ constructor(path: string);
22
+ getEnvironment: () => UiEnvironment;
23
+ setActive(active: boolean): void;
24
+ subscribe: (listener: (event: UiEvent) => void) => () => void;
25
+ private emit;
26
+ private retirePending;
27
+ private setEnvironment;
28
+ private probe;
29
+ private discover;
30
+ private post;
31
+ private recheck;
32
+ private receive;
33
+ private request;
34
+ private wait;
35
+ openSurface: (target: PanelTarget, options: PresentationOptions) => Promise<PresentationResult>;
36
+ getPresentation: (requestId: string, options?: {
37
+ signal?: AbortSignal;
38
+ }) => Promise<PresentationResult>;
39
+ reportSurfaceState: (state: {
40
+ route: string;
41
+ state: "ready" | "blocked";
42
+ code?: string;
43
+ }) => void;
44
+ dispose: () => void;
45
+ }
@@ -0,0 +1,278 @@
1
+ import { createPresentationRequestId } from "./ui.js";
2
+ import { UiError } from "./ui-types.js";
3
+ import { DISCOVERY_TIMEOUT, failure, readUiMessage, uiRequest, validId, validTarget, waitForPresentation } from "./ui-transport.js";
4
+ /** Direct-child transport. Discovery carries no identity and never creates a fallback host. */
5
+ export class EmbeddedUiClient {
6
+ path;
7
+ environment = { mode: "embedded", connection: "connecting" };
8
+ nonce = createPresentationRequestId();
9
+ listeners = new Set();
10
+ pending = new Map();
11
+ channel = null;
12
+ verifying = null;
13
+ disposed = false;
14
+ deadline;
15
+ retries = [];
16
+ state = null;
17
+ active = true;
18
+ offered = false;
19
+ onMessage = (event) => { void this.receive(event); };
20
+ onFocus = () => { void this.recheck(); };
21
+ onKey = (event) => { if (this.active && !event.defaultPrevented && event.key === "Escape")
22
+ this.post({ type: "hosty:ui:dismiss" }); };
23
+ constructor(path) {
24
+ this.path = path;
25
+ window.addEventListener("message", this.onMessage);
26
+ window.addEventListener("focus", this.onFocus);
27
+ window.addEventListener("keydown", this.onKey);
28
+ this.discover();
29
+ if (window.parent !== window.top)
30
+ this.setEnvironment("unsupported", "unsupported_nesting");
31
+ }
32
+ getEnvironment = () => this.environment;
33
+ setActive(active) {
34
+ if (this.active === active)
35
+ return;
36
+ if (!active)
37
+ this.post({ type: "hosty:ui:auth-required" });
38
+ this.active = active;
39
+ this.channel = null;
40
+ this.verifying = null;
41
+ this.nonce = createPresentationRequestId();
42
+ clearTimeout(this.deadline);
43
+ this.retries.forEach(clearTimeout);
44
+ if (!active) {
45
+ for (const [requestId, entry] of this.pending)
46
+ entry.resolve(entry.sent
47
+ ? { status: "uncertain", requestId, code: "presentation_unknown" }
48
+ : { status: "blocked", requestId, code: "activity_required" });
49
+ this.pending.clear();
50
+ this.setEnvironment("blocked", "activity_required");
51
+ }
52
+ else {
53
+ this.setEnvironment("connecting");
54
+ this.discover();
55
+ }
56
+ }
57
+ subscribe = (listener) => {
58
+ this.listeners.add(listener);
59
+ listener({ type: "environment", value: this.environment });
60
+ return () => { this.listeners.delete(listener); };
61
+ };
62
+ emit(event) { if (!this.disposed)
63
+ for (const listener of this.listeners)
64
+ listener(event); }
65
+ retirePending() {
66
+ for (const [requestId, entry] of this.pending)
67
+ entry.resolve(entry.sent
68
+ ? { status: "uncertain", requestId, code: "presentation_unknown" }
69
+ : { status: "unsupported", requestId, code: "host_unavailable" });
70
+ }
71
+ setEnvironment(connection, code) {
72
+ if (this.disposed)
73
+ return;
74
+ this.environment = { mode: "embedded", connection, ...(code ? { code } : {}) };
75
+ this.emit({ type: "environment", value: this.environment });
76
+ }
77
+ probe() {
78
+ if (this.active && !this.disposed && window.parent === window.top && !this.channel)
79
+ window.parent.postMessage({ type: "hosty:ui:probe", version: 1, versions: [1], childNonce: this.nonce }, "*");
80
+ }
81
+ discover() {
82
+ clearTimeout(this.deadline);
83
+ this.retries.forEach(clearTimeout);
84
+ this.deadline = setTimeout(() => {
85
+ if (this.environment.connection === "connecting")
86
+ this.setEnvironment(this.offered ? "blocked" : "unsupported", "host_unavailable");
87
+ }, DISCOVERY_TIMEOUT);
88
+ this.probe();
89
+ this.retries = [150, 500, 1500].map(delay => setTimeout(() => this.probe(), delay));
90
+ }
91
+ post(value) {
92
+ const channel = this.channel;
93
+ if (!channel || !this.active || this.disposed)
94
+ return;
95
+ window.parent.postMessage({ version: 1, childNonce: this.nonce, hostWindowId: channel.hostWindowId, generation: channel.generation, ...value }, channel.origin);
96
+ }
97
+ async recheck() {
98
+ const channel = this.channel;
99
+ if (!this.active || !channel || this.environment.connection !== "connected")
100
+ return false;
101
+ try {
102
+ const own = await uiRequest(this.path, `bootstrap?parentOrigin=${encodeURIComponent(channel.origin)}`);
103
+ if (this.disposed || this.channel !== channel)
104
+ return false;
105
+ if (own.userId !== channel.own.userId || own.coreOrigin !== channel.own.coreOrigin || own.app.installationId !== channel.own.app.installationId
106
+ || own.peer?.installationId !== channel.own.peer?.installationId || own.peer?.appId !== channel.own.peer?.appId)
107
+ throw new UiError("actor_mismatch", "Reconnect using the same Hosty account.");
108
+ return true;
109
+ }
110
+ catch (error) {
111
+ if (this.channel !== channel || this.disposed)
112
+ return false;
113
+ this.post({ type: "hosty:ui:surface-state", route: this.state?.route, state: "auth-required" });
114
+ this.setEnvironment("blocked", error instanceof UiError ? error.code : "ui_unavailable");
115
+ return false;
116
+ }
117
+ }
118
+ async receive(event) {
119
+ const msg = readUiMessage(event.data);
120
+ if (!msg || !this.active || event.source !== window.parent || window.parent !== window.top || event.origin === "null" || this.disposed)
121
+ return;
122
+ if (msg.type === "hosty:ui:probe-request") {
123
+ if (this.channel && event.origin === this.channel.origin && (msg.generation !== this.channel.generation || msg.hostWindowId !== this.channel.hostWindowId)) {
124
+ this.retirePending();
125
+ this.channel = null;
126
+ this.setEnvironment("connecting");
127
+ this.discover();
128
+ }
129
+ if (this.channel) {
130
+ void this.recheck();
131
+ return;
132
+ }
133
+ this.probe();
134
+ return;
135
+ }
136
+ if (msg.childNonce !== this.nonce || !validId(msg.hostWindowId) || !validId(msg.generation))
137
+ return;
138
+ if (msg.type === "hosty:ui:offer") {
139
+ this.offered = true;
140
+ if (this.channel?.origin === event.origin && this.channel.hostWindowId === msg.hostWindowId
141
+ && this.channel.generation === msg.generation && this.environment.connection === "connected")
142
+ return;
143
+ const key = `${this.nonce}:${event.origin}:${msg.hostWindowId}:${msg.generation}`;
144
+ if (this.verifying === key)
145
+ return;
146
+ this.verifying = key;
147
+ try {
148
+ const own = await uiRequest(this.path, `bootstrap?parentOrigin=${encodeURIComponent(event.origin)}`);
149
+ if (this.disposed || !this.active || this.verifying !== key)
150
+ return;
151
+ if (own.version !== 1 || !own.userId || !own.peer?.origins.includes(event.origin))
152
+ throw new UiError("host_untrusted", "Hosty cannot verify this host.");
153
+ const previous = this.channel;
154
+ if (previous && (previous.origin !== event.origin || previous.hostWindowId !== msg.hostWindowId || previous.generation !== msg.generation))
155
+ this.retirePending();
156
+ this.channel = { origin: event.origin, hostWindowId: msg.hostWindowId, generation: msg.generation, own };
157
+ this.post({ type: "hosty:ui:accept", coreOrigin: own.coreOrigin, userId: own.userId, appId: own.app.appId, installationId: own.app.installationId });
158
+ }
159
+ catch (error) {
160
+ if (this.disposed || !this.active || this.verifying !== key)
161
+ return;
162
+ const code = error instanceof UiError ? error.code : "ui_unavailable";
163
+ this.setEnvironment(code.startsWith("unsupported") ? "unsupported" : "blocked", code);
164
+ }
165
+ finally {
166
+ if (this.verifying === key)
167
+ this.verifying = null;
168
+ }
169
+ return;
170
+ }
171
+ const channel = this.channel;
172
+ if (!channel || event.origin !== channel.origin || msg.hostWindowId !== channel.hostWindowId || msg.generation !== channel.generation)
173
+ return;
174
+ if (msg.type === "hosty:ui:connected") {
175
+ const own = channel.own;
176
+ if (msg.code || msg.coreOrigin !== own.coreOrigin || msg.userId !== own.userId || msg.appId !== own.peer?.appId || msg.installationId !== own.peer?.installationId) {
177
+ this.setEnvironment("blocked", "actor_mismatch");
178
+ return;
179
+ }
180
+ clearTimeout(this.deadline);
181
+ this.setEnvironment("connected");
182
+ if (this.state)
183
+ this.reportSurfaceState(this.state);
184
+ return;
185
+ }
186
+ if (this.environment.connection !== "connected")
187
+ return;
188
+ if (msg.type === "hosty:ui:result" && validId(msg.requestId) && validResult(msg.result, msg.requestId)) {
189
+ const result = msg.result;
190
+ this.pending.get(msg.requestId)?.resolve(result);
191
+ this.emit({ type: "presentation", value: result });
192
+ }
193
+ }
194
+ async request(requestId, message, signal) {
195
+ if (!validId(requestId))
196
+ return { status: "rejected", requestId, code: "invalid_presentation" };
197
+ if (!this.active)
198
+ return { status: "blocked", requestId, code: "activity_required" };
199
+ if (signal?.aborted)
200
+ return { status: "uncertain", requestId, code: "presentation_unknown" };
201
+ const target = message.target;
202
+ const input = target ? JSON.stringify([target.panel.appId, target.panel.installationId, target.panel.panelId, target.route ?? null]) : null;
203
+ const existing = this.pending.get(requestId);
204
+ if (existing) {
205
+ if (input !== null && existing.input !== null && input !== existing.input)
206
+ return { status: "rejected", requestId, code: "request_conflict" };
207
+ // A status response cannot satisfy an opening dispatched later. Finish that query first;
208
+ // status queried during an opening can simply share the in-flight operation's result.
209
+ if (input !== null && existing.input === null) {
210
+ const result = await this.wait(requestId, existing, signal);
211
+ if (this.pending.get(requestId) === existing)
212
+ return result;
213
+ return this.request(requestId, message, signal);
214
+ }
215
+ return this.wait(requestId, existing, signal);
216
+ }
217
+ if (this.pending.size >= 4)
218
+ return { status: "blocked", requestId, code: "presentation_busy" };
219
+ let settle;
220
+ const operation = new Promise(resolve => { settle = resolve; });
221
+ const entry = { input, operation, waiters: 0, sent: false, resolve: result => {
222
+ if (this.pending.get(requestId) === entry)
223
+ this.pending.delete(requestId);
224
+ settle(result);
225
+ } };
226
+ // Reserve identity before asynchronous revalidation so racing callers cannot share
227
+ // different payloads or overwrite each other's completion/cleanup handlers.
228
+ this.pending.set(requestId, entry);
229
+ void this.recheck().then(verified => {
230
+ if (this.pending.get(requestId) !== entry)
231
+ return;
232
+ if (!verified)
233
+ entry.resolve({ status: this.environment.connection === "blocked" ? "blocked" : "unsupported", requestId, code: this.environment.code ?? "host_unavailable" });
234
+ else {
235
+ entry.sent = true;
236
+ this.post({ ...message, requestId });
237
+ }
238
+ });
239
+ return this.wait(requestId, entry, signal);
240
+ }
241
+ wait(requestId, entry, signal) {
242
+ entry.waiters++;
243
+ return waitForPresentation(entry.operation, requestId, signal).finally(() => {
244
+ // Ending a caller's wait does not cancel a sent message. Keep its reservation until
245
+ // its response arrives or the channel is retired, so it cannot settle newer work.
246
+ if (--entry.waiters === 0 && !entry.sent && this.pending.get(requestId) === entry)
247
+ this.pending.delete(requestId);
248
+ });
249
+ }
250
+ openSurface = (target, options) => validTarget(target) && options.placement === "side"
251
+ ? this.request(options.requestId, { type: "hosty:ui:open", target, placement: "side" }, options.signal)
252
+ : Promise.resolve({ status: "rejected", requestId: options.requestId, code: "invalid_presentation" });
253
+ getPresentation = (requestId, options) => this.request(requestId, { type: "hosty:ui:status" }, options?.signal);
254
+ reportSurfaceState = (state) => {
255
+ this.state = state;
256
+ if (this.environment.connection === "connected")
257
+ this.post({ type: "hosty:ui:surface-state", ...state });
258
+ };
259
+ dispose = () => {
260
+ this.disposed = true;
261
+ clearTimeout(this.deadline);
262
+ this.retries.forEach(clearTimeout);
263
+ window.removeEventListener("message", this.onMessage);
264
+ window.removeEventListener("focus", this.onFocus);
265
+ window.removeEventListener("keydown", this.onKey);
266
+ for (const [id, entry] of this.pending)
267
+ entry.resolve(failure(id, new UiError("host_unavailable", "Host disconnected.")));
268
+ this.pending.clear();
269
+ this.listeners.clear();
270
+ };
271
+ }
272
+ function validResult(value, id) {
273
+ if (!value || typeof value !== "object")
274
+ return false;
275
+ const v = value;
276
+ return v.requestId === id && (v.status === "opened" ? validTarget(v.target) && ["loading", "ready", "unreported", "auth-required"].includes(v.readiness)
277
+ : ["blocked", "unsupported", "rejected", "uncertain", "unknown"].includes(v.status) && "code" in v && typeof v.code === "string");
278
+ }
@@ -0,0 +1,30 @@
1
+ import { type HostyResolvedTheme, type HostyThemePreference } from "./theme.js";
2
+ import type { UiFrameSurface } from "./ui-types.js";
3
+ export declare function parseFrameMcpReview(event: {
4
+ source: unknown;
5
+ origin: string;
6
+ data: unknown;
7
+ }, frame: unknown, src: string, appId: string): {
8
+ appId: string;
9
+ targetAppId: string;
10
+ } | null;
11
+ export declare function frameSandbox(policy: UiFrameSurface["framePolicy"]): string;
12
+ export type HostyAppFrameProps = {
13
+ surface: UiFrameSurface;
14
+ title: string;
15
+ src?: string;
16
+ className?: string;
17
+ theme?: HostyResolvedTheme;
18
+ themePreference?: HostyThemePreference;
19
+ standaloneHref?: string | null;
20
+ onAskAssistant?: (text: string, appId: string) => void;
21
+ onAttention?: (count: number) => void;
22
+ onAuthRequired?: (appId: string) => void;
23
+ onMessage?: (event: MessageEvent, frame: Window | null) => void;
24
+ outbound?: {
25
+ message: unknown;
26
+ nonce: number;
27
+ } | null;
28
+ };
29
+ /** Shared frame coordinator. No delegated-token responder and no authority inferred from messages. */
30
+ export declare function HostyAppFrame(props: HostyAppFrameProps): import("react").JSX.Element;
@@ -0,0 +1,104 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useContext, useEffect, useLayoutEffect, useRef, useState } from "react";
4
+ import { UiHostContext } from "./ui-context.js";
5
+ import { appendThemeLaunchParams, parseActiveFrameAskAssistant, parseActiveFrameAttention, parseActiveFrameAuthRequired, parseActiveFramePermissionReview } from "./embedder.js";
6
+ import { createShellThemeMessage } from "./theme.js";
7
+ import { permissionReviewUrl } from "./permissions.js";
8
+ export function parseFrameMcpReview(event, frame, src, appId) {
9
+ if (!frame || event.source !== frame || !event.data || typeof event.data !== "object")
10
+ return null;
11
+ try {
12
+ if (event.origin !== new URL(src).origin)
13
+ return null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ const data = event.data;
19
+ return data.type === "hosty:request-mcp-review" && typeof data.targetAppId === "string" && /^[a-zA-Z0-9._:-]{1,128}$/.test(data.targetAppId)
20
+ ? { appId, targetAppId: data.targetAppId } : null;
21
+ }
22
+ export function frameSandbox(policy) {
23
+ return "allow-scripts allow-same-origin allow-forms allow-popups allow-downloads" + (policy.popupEscape ? " allow-popups-to-escape-sandbox" : "");
24
+ }
25
+ /** Shared frame coordinator. No delegated-token responder and no authority inferred from messages. */
26
+ export function HostyAppFrame(props) {
27
+ const { surface, title, className, standaloneHref, theme, themePreference } = props;
28
+ const host = useContext(UiHostContext);
29
+ const ref = useRef(null);
30
+ const callbacks = useRef(props);
31
+ callbacks.current = props;
32
+ const [review, setReview] = useState(null);
33
+ const [loaded, setLoaded] = useState(false);
34
+ const [connection, setConnection] = useState({ ready: false, generation: 0 });
35
+ const [appearance, setAppearance] = useState({ theme: "light", preference: "system" });
36
+ useEffect(() => {
37
+ const read = () => {
38
+ const root = document.documentElement;
39
+ setAppearance({
40
+ theme: root.classList.contains("dark") || root.dataset.hostyTheme === "dark" ? "dark" : "light",
41
+ preference: root.dataset.hostyThemePreference === "dark" ? "dark" : root.dataset.hostyThemePreference === "light" ? "light" : "system"
42
+ });
43
+ };
44
+ read();
45
+ const observer = new MutationObserver(read);
46
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class", "data-hosty-theme", "data-hosty-theme-preference"] });
47
+ return () => observer.disconnect();
48
+ }, []);
49
+ const resolvedTheme = theme ?? appearance.theme, preference = themePreference ?? appearance.preference;
50
+ const source = props.src ?? new URL(surface.route, surface.origin).href;
51
+ // Theme changes are messages; they must not replace a document with an unsent draft.
52
+ const [launch] = useState(() => {
53
+ const url = new URL(source);
54
+ url.searchParams.set("hosty_launch", "embedded");
55
+ return appendThemeLaunchParams(url.href, resolvedTheme, preference);
56
+ });
57
+ const frameId = JSON.stringify([surface, source]);
58
+ useLayoutEffect(() => {
59
+ const frame = ref.current;
60
+ if (!frame || !host)
61
+ return;
62
+ return host.registerFrame(frame, surface, ready => setConnection(current => ({ ready, generation: current.generation + (ready ? 1 : 0) })));
63
+ }, [host, frameId]); // Caller keys the frame by this immutable surface.
64
+ useEffect(() => {
65
+ if (loaded)
66
+ ref.current?.contentWindow?.postMessage(createShellThemeMessage(resolvedTheme, preference), surface.origin);
67
+ }, [loaded, resolvedTheme, preference, surface.origin]);
68
+ useEffect(() => {
69
+ const handle = (event) => {
70
+ const frame = ref.current?.contentWindow ?? null;
71
+ if (!frame || event.source !== frame || event.origin !== surface.origin)
72
+ return;
73
+ const current = callbacks.current;
74
+ if (parseActiveFrameAuthRequired(event, frame, source, surface.appId))
75
+ current.onAuthRequired?.(surface.appId);
76
+ const ask = parseActiveFrameAskAssistant(event, frame, source);
77
+ if (ask)
78
+ current.onAskAssistant?.(ask.text, surface.appId);
79
+ const attention = parseActiveFrameAttention(event, frame, source);
80
+ if (attention !== null)
81
+ current.onAttention?.(attention);
82
+ const permission = parseActiveFramePermissionReview(event, frame, source, surface.appId);
83
+ const mcp = parseFrameMcpReview(event, frame, source, surface.appId);
84
+ const core = host?.getSnapshot().bootstrap?.coreOrigin;
85
+ if (core && (permission || mcp)) {
86
+ const url = mcp ? new URL(`/install/agents/${encodeURIComponent(mcp.appId)}/${encodeURIComponent(mcp.targetAppId)}`, core).href : permissionReviewUrl(core, surface.appId);
87
+ setReview(url);
88
+ window.open(url, "_blank", "noopener,noreferrer");
89
+ }
90
+ current.onMessage?.(event, frame);
91
+ };
92
+ window.addEventListener("message", handle);
93
+ return () => window.removeEventListener("message", handle);
94
+ }, [host, frameId, source, surface]);
95
+ const outboundReady = host ? connection.ready : loaded;
96
+ useEffect(() => {
97
+ if (outboundReady && props.outbound)
98
+ ref.current?.contentWindow?.postMessage(props.outbound.message, surface.origin);
99
+ }, [outboundReady, connection.generation, props.outbound?.nonce, surface.origin]);
100
+ const insecure = typeof window !== "undefined" && window.location.protocol === "https:" && new URL(surface.origin).protocol === "http:";
101
+ if (insecure)
102
+ return _jsxs("div", { role: "status", className: "hosty-panel-notice", children: [_jsxs("strong", { children: [title, " needs HTTPS to appear here"] }), _jsx("p", { children: "This app publishes an HTTP address. Configure its public HTTPS origin in Hosty." }), /^(localhost|127\.|\[::1\])/.test(new URL(surface.origin).hostname) && _jsx("p", { children: "This local address is reachable only from the Hosty computer." }), standaloneHref && _jsx("a", { href: standaloneHref, target: "_blank", rel: "noopener noreferrer", children: "Open separately" })] });
103
+ return _jsxs(_Fragment, { children: [review && _jsx("a", { className: "hosty-panel-review", href: review, target: "_blank", rel: "noopener noreferrer", children: "Review access in Core" }), _jsx("iframe", { ref: ref, className: className, title: title, src: launch, sandbox: frameSandbox(surface.framePolicy), allow: surface.framePolicy.microphone ? "clipboard-write; microphone 'src'" : "clipboard-write", style: { border: 0, height: "100%", width: "100%", colorScheme: resolvedTheme }, onLoad: () => setLoaded(true) })] });
104
+ }
@@ -0,0 +1,80 @@
1
+ import { type UiClient } from "./ui.js";
2
+ import { type PanelRef, type PanelTarget, type PresentationOptions, type PresentationResult, type UiBootstrap, type UiEnvironment, type UiEvent, type UiFrameSurface, type UiPanel, type UiResolvedPanel, type SurfaceReadiness } from "./ui-types.js";
3
+ export type UiTool = {
4
+ key: string;
5
+ target: PanelTarget;
6
+ resolved: UiResolvedPanel;
7
+ readiness: SurfaceReadiness;
8
+ attention: number;
9
+ };
10
+ export type UiHostSnapshot = {
11
+ bootstrap: UiBootstrap | null;
12
+ catalog: UiPanel[];
13
+ tools: UiTool[];
14
+ selected: PanelRef | null;
15
+ expanded: boolean;
16
+ width: number;
17
+ error: {
18
+ code: string;
19
+ message: string;
20
+ } | null;
21
+ };
22
+ /** One top-level composition owner. All Core traffic goes through this app's authenticated adapter. */
23
+ export declare function createUiHost(options?: {
24
+ uiPath?: string;
25
+ active?: boolean;
26
+ }): UiHost;
27
+ export declare class UiHost implements UiClient {
28
+ private readonly path;
29
+ private active;
30
+ private snapshot;
31
+ private environment;
32
+ private readonly windowId;
33
+ private readonly listeners;
34
+ private readonly events;
35
+ private readonly frames;
36
+ private readonly ledger;
37
+ private readonly toolQueues;
38
+ private selectionGeneration;
39
+ private activityGeneration;
40
+ private activityReady;
41
+ private disposed;
42
+ private workspace;
43
+ private refreshing;
44
+ private readonly onMessage;
45
+ private readonly onFocus;
46
+ constructor(path: string, active?: boolean);
47
+ getEnvironment: () => UiEnvironment;
48
+ setActive(active: boolean): void;
49
+ private checkActivity;
50
+ claimWorkspace: (owner: object) => () => void;
51
+ getSnapshot: () => UiHostSnapshot;
52
+ subscribeSnapshot: (listener: () => void) => () => void;
53
+ subscribe: (listener: (event: UiEvent) => void) => () => void;
54
+ private emit;
55
+ private update;
56
+ private setEnvironment;
57
+ private bootstrap;
58
+ refresh: () => Promise<void>;
59
+ openSurface: (target: PanelTarget, options: PresentationOptions) => Promise<PresentationResult>;
60
+ private open;
61
+ getPresentation: (id: string, options?: {
62
+ signal?: AbortSignal;
63
+ }) => Promise<PresentationResult>;
64
+ private status;
65
+ selectPanel: (ref: PanelRef) => Promise<PresentationResult>;
66
+ setExpanded: (expanded: boolean) => void;
67
+ setWidth: (width: number) => void;
68
+ setAttention: (panel: PanelRef, attention: number) => void;
69
+ reportSurfaceState: (_state: {
70
+ route: string;
71
+ state: "ready" | "blocked";
72
+ code?: string;
73
+ }) => void;
74
+ private forgetFrameRequests;
75
+ registerFrame: (element: HTMLIFrameElement, surface: UiFrameSurface, onConnection?: (connected: boolean) => void) => (() => void);
76
+ private post;
77
+ private reportFrame;
78
+ private receive;
79
+ dispose: () => void;
80
+ }