@hosty-sdk/app 0.25.12 → 0.27.22

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 (47) hide show
  1. package/README.md +181 -19
  2. package/dist/background-server.d.ts +16 -0
  3. package/dist/background-server.js +52 -0
  4. package/dist/browser-auth.d.ts +2 -0
  5. package/dist/browser-auth.js +5 -0
  6. package/dist/continuity-browser.d.ts +11 -0
  7. package/dist/continuity-browser.js +92 -0
  8. package/dist/continuity.d.ts +9 -0
  9. package/dist/continuity.js +9 -0
  10. package/dist/index.d.ts +9 -6
  11. package/dist/index.js +10 -1
  12. package/dist/next.d.ts +4 -0
  13. package/dist/next.js +29 -0
  14. package/dist/overlay-react.d.ts +9 -1
  15. package/dist/overlay-react.js +6 -4
  16. package/dist/react.d.ts +3 -0
  17. package/dist/react.js +18 -1
  18. package/dist/server.d.ts +2 -0
  19. package/dist/server.js +2 -0
  20. package/dist/session-server.d.ts +2 -0
  21. package/dist/ui-composition.d.ts +71 -0
  22. package/dist/ui-composition.js +232 -0
  23. package/dist/ui-context.d.ts +2 -0
  24. package/dist/ui-context.js +1 -0
  25. package/dist/ui-embedded.d.ts +18 -1
  26. package/dist/ui-embedded.js +123 -17
  27. package/dist/ui-frame-layer.d.ts +36 -0
  28. package/dist/ui-frame-layer.js +88 -0
  29. package/dist/ui-frame.d.ts +2 -0
  30. package/dist/ui-frame.js +43 -7
  31. package/dist/ui-host.d.ts +62 -15
  32. package/dist/ui-host.js +666 -76
  33. package/dist/ui-navigation-react.d.ts +17 -0
  34. package/dist/ui-navigation-react.js +89 -0
  35. package/dist/ui-react.d.ts +13 -3
  36. package/dist/ui-react.js +127 -50
  37. package/dist/ui-server.d.ts +3 -1
  38. package/dist/ui-server.js +14 -2
  39. package/dist/ui-transport.d.ts +2 -2
  40. package/dist/ui-transport.js +1 -1
  41. package/dist/ui-types.d.ts +88 -0
  42. package/dist/ui-types.js +1 -0
  43. package/dist/ui-workspaces.d.ts +54 -0
  44. package/dist/ui-workspaces.js +3 -0
  45. package/dist/ui.d.ts +7 -1
  46. package/dist/ui.js +22 -0
  47. package/package.json +17 -1
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useEffect, useLayoutEffect, useRef, useState } from "react";
4
4
  import { createPortal } from "react-dom";
5
5
  import { useAppIdentity } from "./react.js";
@@ -30,7 +30,7 @@ function sessionReady(session) {
30
30
  return session?.setup === "ready" && (!session.activityRequired || session.recoveryReady) && !activityExpired(session);
31
31
  }
32
32
  /** Mount once around protected content. Presentation and recovery belong to Hosty. */
33
- export function HostyOverlay({ children, probePath = "/api/hosty/session", appCodePath = "/api/auth/app-code", uiPath, panelLayout = "auto" }) {
33
+ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCodePath = "/api/auth/app-code", uiPath, panelLayout = "auto", localContent, chrome, navigation }) {
34
34
  const actor = useRef(null);
35
35
  const changingActor = useRef(false);
36
36
  const [session, setSession] = useState(null);
@@ -80,7 +80,7 @@ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCo
80
80
  setReviewing(false);
81
81
  }, [session?.setup]);
82
82
  useEffect(() => {
83
- // No keepalive polling. Focus and a pending Core review are the only readiness refreshes.
83
+ // Continuous sessions own their maintenance; this also observes pending permission reviews.
84
84
  let loading = false;
85
85
  const refresh = () => {
86
86
  if (loading || !actor.current || activity.pending || (activity.needed && !renewedAwaitingReadiness.current))
@@ -166,7 +166,9 @@ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCo
166
166
  action = activity.renew;
167
167
  actionLabel = "Renew access";
168
168
  }
169
- return _jsxs(_Fragment, { children: [_jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { uiPath: uiPath, panelLayout: panelLayout, active: ready, children: mounted.current ? children : null }) }), !ready && _jsx(OverlaySurface, { pending: pending, title: title, message: message, action: action, actionLabel: actionLabel, reviewUrl: reviewUrl })] });
169
+ const localOffline = (identity.ui.kind === "unavailable" || (identity.ui.kind === "active" && session?.setup === "unavailable"))
170
+ && localContent !== undefined;
171
+ return _jsxs(_Fragment, { children: [localOffline && _jsxs("div", { "data-hosty-local-content": true, children: [_jsx("p", { role: "status", children: "Hosty is unavailable. Local content remains available." }), localContent] }), _jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { chrome: chrome, navigation: navigation, uiPath: uiPath, panelLayout: panelLayout, active: ready, children: mounted.current ? children : null }) }), !ready && !localOffline && _jsx(OverlaySurface, { pending: pending, title: title, message: message, action: action, actionLabel: actionLabel, reviewUrl: reviewUrl })] });
170
172
  }
171
173
  function OverlaySurface({ pending, title, message, action, actionLabel, reviewUrl }) {
172
174
  const [portal, setPortal] = useState(null);
package/dist/react.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { type AppContinuity } from "./continuity.js";
1
2
  import type { AppAuthProtocol } from "./app-code.js";
2
3
  import type { ReactNode } from "react";
3
4
  import { type AppLaunchMode, type AppSessionStatus } from "./index.js";
@@ -71,6 +72,7 @@ export declare function useAppIdentity({ probePath, appCodePath, onSession, }?:
71
72
  appAuthProtocol: AppAuthProtocol;
72
73
  activeUntil?: string | null;
73
74
  activityRequired?: boolean;
75
+ continuity?: AppContinuity;
74
76
  } | null;
75
77
  hasBeenActive: boolean;
76
78
  refresh: () => Promise<unknown>;
@@ -107,3 +109,4 @@ export declare function HostThemeBridge({ followSystem, onTheme }?: HostThemeBri
107
109
  export { MissingPermissionsNotice } from "./permissions-react.js";
108
110
  export { AppActivityBridge } from "./activity-react.js";
109
111
  export { HostyOverlay } from "./overlay-react.js";
112
+ export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
package/dist/react.js CHANGED
@@ -1,8 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { captureChromeHints } from "./ui-composition.js";
3
4
  // React slice: the client identity bridge — probe, silent recovery, and the fallback
4
5
  // cards. Prop-less by design: the app id and browser-reachable Core origin arrive in the
5
6
  // probe response's `recovery` field (request-time values, never build-time props).
7
+ import { readAppContinuity } from "./continuity.js";
8
+ import { configureAppContinuity } from "./continuity-browser.js";
6
9
  import { AppActivityBridge } from "./activity-react.js";
7
10
  import { AuthNotice, AuthNoticeButton, authNoticeActionStyle } from "./auth-notice.js";
8
11
  import { useEffect, useRef, useState } from "react";
@@ -136,6 +139,7 @@ function currentLaunchMode() {
136
139
  */
137
140
  export function HostLaunchBridge() {
138
141
  useEffect(() => {
142
+ captureChromeHints();
139
143
  const url = new URL(window.location.href);
140
144
  const param = url.searchParams.get(LAUNCH_MODE_PARAM);
141
145
  const { mode, fromParam } = resolveLaunchMode({
@@ -203,6 +207,18 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
203
207
  const exchangeRef = useRef(null);
204
208
  const onSessionRef = useRef(onSession);
205
209
  useEffect(() => { onSessionRef.current = onSession; }, [onSession]);
210
+ const continuity = activity?.continuity;
211
+ const continuityActor = session?.userId;
212
+ useEffect(() => {
213
+ if (!continuity || !activity?.openUrl || !continuityActor)
214
+ return;
215
+ return configureAppContinuity({ coreOrigin: new URL(activity.openUrl).origin, authority: continuity,
216
+ probe: async () => {
217
+ const next = await refreshRef.current();
218
+ return next?.status === "active" && next.userId === continuityActor
219
+ && (!next.hosty || (next.hosty.version === 1 && next.hosty.setup === "ready"));
220
+ } });
221
+ }, [continuity?.grantId, activity?.openUrl, continuityActor]);
206
222
  useEffect(() => {
207
223
  let cancelled = false;
208
224
  let probeSequence = 0;
@@ -263,7 +279,7 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
263
279
  forgetRejectedAppGrant(rejectionCode, revision);
264
280
  if (openUrl && protocol)
265
281
  setActivity({ openUrl, appAuthProtocol: protocol,
266
- activeUntil: activityMetadata?.activeUntil, activityRequired: activityMetadata?.activityRequired });
282
+ activeUntil: activityMetadata?.activeUntil, activityRequired: activityMetadata?.activityRequired, continuity: readAppContinuity(activityMetadata?.continuity) });
267
283
  const initialLoad = firstProbe.current;
268
284
  firstProbe.current = false;
269
285
  if (silentError.current === "access_denied") {
@@ -551,3 +567,4 @@ export function HostThemeBridge({ followSystem = true, onTheme } = {}) {
551
567
  export { MissingPermissionsNotice } from "./permissions-react.js";
552
568
  export { AppActivityBridge } from "./activity-react.js";
553
569
  export { HostyOverlay } from "./overlay-react.js";
570
+ export { HostyNavigation, HostyNavigationBoundary, useHostyChrome, useHostyNavigation } from "./ui-navigation-react.js";
package/dist/server.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import "server-only";
2
+ import { type AppContinuity } from "./continuity.js";
2
3
  import { type AppAuthProtocol } from "./app-code.js";
3
4
  export { isValidCodeVerifier } from "./app-code.js";
4
5
  import { type AppSessionFailureStatus, type AppSessionStatus, type SessionRecoveryParams } from "./index.js";
@@ -25,6 +26,7 @@ export interface HostIdentity {
25
26
  expiresAt: string | null;
26
27
  activeUntil?: string | null;
27
28
  activityRequired?: boolean;
29
+ continuity?: AppContinuity;
28
30
  }
29
31
  /** Structured detail for a failed resolution: the Core HTTP status (null when Core was
30
32
  * never reached) plus a machine-readable code and human-readable message. Codes pass
package/dist/server.js CHANGED
@@ -1,6 +1,7 @@
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";
5
6
  import { createHostyUiResponse } from "./ui-server.js";
6
7
  import { resolveAppAuthProtocol, appAuthProtocolMinimum, isValidCodeVerifier } from "./app-code.js";
@@ -221,6 +222,7 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
221
222
  expiresAt,
222
223
  activeUntil: readString(payload.activeUntil),
223
224
  activityRequired: payload.activityRequired === true,
225
+ continuity: readAppContinuity(payload.continuity),
224
226
  };
225
227
  const expiresAtMs = expiresAt ? Date.parse(expiresAt) : Number.NaN;
226
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,71 @@
1
+ import { type UiChromeOverrides, type UiChromeSnapshot, type UiChromeSource, type UiEnvironment, type UiNavigationCatalog, type UiNavigationEntry, type UiNavigationSnapshot, type UiRouterAdapter } from "./ui-types.js";
2
+ export declare const CHROME_HINT_ATTRIBUTE = "data-hosty-chrome-hints";
3
+ export declare function parseChromeHints(search: string): UiChromeOverrides & {
4
+ legacy?: "show" | "hide";
5
+ };
6
+ export declare function validChromeOverrides(value: unknown): value is UiChromeOverrides;
7
+ export declare function resolveChrome(defaults: UiChromeOverrides, hints: ReturnType<typeof parseChromeHints>, host?: UiChromeOverrides): {
8
+ navigation: "hide" | "show";
9
+ enabled: boolean;
10
+ launcher: "hide" | "show";
11
+ sources: {
12
+ navigation: UiChromeSource;
13
+ enabled: UiChromeSource;
14
+ launcher: UiChromeSource;
15
+ };
16
+ };
17
+ export declare const INITIAL_CHROME: UiChromeSnapshot;
18
+ export declare const INITIAL_NAVIGATION: UiNavigationSnapshot;
19
+ /** Presentation hints are captured only for this document, never from inherited session storage. */
20
+ export declare function captureChromeHints(): string;
21
+ export declare function withChromeHints(href: string, chrome: UiChromeOverrides): string;
22
+ export { chromeBootstrapScript } from "./index.js";
23
+ export declare function activeNavigationKey(entries: readonly UiNavigationEntry[], endpoint: string | undefined, route: string): string | null;
24
+ /** Shared observable state for local and embedded clients; it owns no frame or identity authority. */
25
+ export declare class UiCompositionModel {
26
+ private readonly path;
27
+ private defaults;
28
+ private chrome;
29
+ private navigation;
30
+ private readonly chromeListeners;
31
+ private readonly navigationListeners;
32
+ private readonly hints;
33
+ private overrides;
34
+ private hostRevision;
35
+ private hostGeneration;
36
+ private rootDisabled;
37
+ private capabilities;
38
+ private environment;
39
+ private router?;
40
+ private stopRouter?;
41
+ private endpoint?;
42
+ private epoch;
43
+ private disposed;
44
+ private refreshing;
45
+ constructor(path: string, mode: UiEnvironment["mode"], defaults?: UiChromeOverrides);
46
+ getChrome: () => UiChromeSnapshot;
47
+ subscribeChrome: (listener: () => void) => () => void;
48
+ getNavigation: () => Readonly<{
49
+ status: "loading" | "ready" | "unavailable";
50
+ catalog: UiNavigationCatalog | null;
51
+ entries: readonly UiNavigationEntry[];
52
+ activeKey: string | null;
53
+ code?: string;
54
+ }>;
55
+ subscribeNavigation: (listener: () => void) => () => void;
56
+ configure(defaults: UiChromeOverrides): void;
57
+ setEnvironment(environment: UiEnvironment): void;
58
+ setHostPolicy(generation: string, revision: number, policy: UiChromeOverrides, rootDisabled: boolean, endpoint?: string): void;
59
+ setLegacyHost(): void;
60
+ private publishChrome;
61
+ attachRouter(router?: UiRouterAdapter): void;
62
+ private updateNavigation;
63
+ currentRoute: () => string;
64
+ currentEndpoint: () => string | undefined;
65
+ isLocalEntry(entry: UiNavigationEntry): boolean;
66
+ updateRoute: () => void;
67
+ refreshNavigation: () => Promise<void>;
68
+ resetNavigation(): void;
69
+ navigateLocal(entry: UiNavigationEntry): Promise<boolean>;
70
+ dispose(): void;
71
+ }
@@ -0,0 +1,232 @@
1
+ import { UI_COMPOSITION_CAPABILITY, UiError, isSurfaceRoute } from "./ui-types.js";
2
+ import { canonicalSurfaceRoute, uiRequest } from "./ui-transport.js";
3
+ export const CHROME_HINT_ATTRIBUTE = "data-hosty-chrome-hints";
4
+ const navigationAttribute = "data-hosty-navigation";
5
+ const fields = ["hosty_navigation", "hosty_tools", "hosty_tool_launcher", "hosty_launch"];
6
+ export function parseChromeHints(search) {
7
+ const params = new URLSearchParams(search);
8
+ const one = (key) => params.getAll(key).length === 1 ? params.get(key) : null;
9
+ const navigation = one(fields[0]), enabled = one(fields[1]), launcher = one(fields[2]), launch = one(fields[3]);
10
+ return { ...(navigation === "show" || navigation === "hide" ? { navigation } : {}),
11
+ tools: { ...(enabled === "enabled" || enabled === "disabled" ? { enabled: enabled === "enabled" } : {}),
12
+ ...(launcher === "show" || launcher === "hide" ? { launcher } : {}) },
13
+ ...(launch === "embedded" || launch === "native" ? { legacy: "hide" } : launch === "standalone" ? { legacy: "show" } : {}) };
14
+ }
15
+ export function validChromeOverrides(value) {
16
+ if (!value || typeof value !== "object" || Array.isArray(value))
17
+ return false;
18
+ const v = value;
19
+ if (Object.keys(v).some(k => !["navigation", "tools"].includes(k)))
20
+ return false;
21
+ if (v.navigation !== undefined && v.navigation !== "show" && v.navigation !== "hide")
22
+ return false;
23
+ if (v.tools === undefined)
24
+ return true;
25
+ return !!v.tools && typeof v.tools === "object" && !Array.isArray(v.tools)
26
+ && Object.keys(v.tools).every(k => k === "enabled" || k === "launcher")
27
+ && (v.tools.enabled === undefined || typeof v.tools.enabled === "boolean")
28
+ && (v.tools.launcher === undefined || ["show", "hide"].includes(v.tools.launcher));
29
+ }
30
+ export function resolveChrome(defaults, hints, host = {}) {
31
+ function leaf(values, fallback) {
32
+ return values.find(([value]) => value !== undefined) ?? [fallback, "default"];
33
+ }
34
+ const [navigation, n] = leaf([[host.navigation, "host"], [hints.navigation, "url"], [defaults.navigation, "app"], [hints.legacy, "legacy"]], "show");
35
+ const [enabled, e] = leaf([[host.tools?.enabled, "host"], [hints.tools?.enabled, "url"], [defaults.tools?.enabled, "app"]], true);
36
+ const [launcher, l] = leaf([[host.tools?.launcher, "host"], [hints.tools?.launcher, "url"], [defaults.tools?.launcher, "app"]], "show");
37
+ return { navigation, enabled, launcher, sources: { navigation: n, enabled: e, launcher: l } };
38
+ }
39
+ export const INITIAL_CHROME = Object.freeze({ navigation: "show", tools: Object.freeze({ enabled: true, launcher: "show", owner: "unavailable" }),
40
+ revision: 0, hostRevision: -1, rootDisabled: false, stale: false, connection: "connecting", capabilities: Object.freeze([]),
41
+ sources: Object.freeze({ navigation: "default", enabled: "default", launcher: "default" }) });
42
+ export const INITIAL_NAVIGATION = Object.freeze({ status: "loading", catalog: null, entries: Object.freeze([]), activeKey: null });
43
+ /** Presentation hints are captured only for this document, never from inherited session storage. */
44
+ export function captureChromeHints() {
45
+ if (typeof document === "undefined")
46
+ return "";
47
+ const root = document.documentElement;
48
+ const captured = root.getAttribute(CHROME_HINT_ATTRIBUTE);
49
+ if (captured !== null)
50
+ return captured;
51
+ const params = new URLSearchParams(window.location.search), hints = new URLSearchParams();
52
+ for (const key of fields)
53
+ for (const value of params.getAll(key))
54
+ hints.append(key, value);
55
+ root.setAttribute(CHROME_HINT_ATTRIBUTE, hints.toString());
56
+ return hints.toString();
57
+ }
58
+ export function withChromeHints(href, chrome) {
59
+ const url = new URL(href, typeof location === "undefined" ? "https://surface.invalid" : location.href);
60
+ for (const key of fields.slice(0, 3))
61
+ url.searchParams.delete(key);
62
+ if (chrome.navigation !== undefined)
63
+ url.searchParams.set(fields[0], chrome.navigation);
64
+ if (chrome.tools?.enabled !== undefined)
65
+ url.searchParams.set(fields[1], chrome.tools.enabled ? "enabled" : "disabled");
66
+ if (chrome.tools?.launcher !== undefined)
67
+ url.searchParams.set(fields[2], chrome.tools.launcher);
68
+ return url.href;
69
+ }
70
+ export { chromeBootstrapScript } from "./index.js";
71
+ export function activeNavigationKey(entries, endpoint, route) {
72
+ const canonical = canonicalSurfaceRoute(route);
73
+ if (!canonical || !endpoint)
74
+ return null;
75
+ const current = new URL(canonical, "https://surface.invalid");
76
+ const candidates = entries.flatMap((entry, index) => {
77
+ if (entry.endpoint !== endpoint || !entry.route)
78
+ return [];
79
+ const normalized = canonicalSurfaceRoute(entry.route);
80
+ if (!normalized)
81
+ return [];
82
+ const declared = new URL(normalized, current.origin), path = declared.pathname.replace(/\/$/, "") || "/";
83
+ if (declared.search && declared.search !== current.search || declared.hash && declared.hash !== current.hash)
84
+ return [];
85
+ if (current.pathname !== declared.pathname && (path === "/" || !current.pathname.startsWith(path + "/")))
86
+ return [];
87
+ return [{ key: entry.key, rank: normalized === canonical ? Infinity : path.length, index }];
88
+ });
89
+ return candidates.sort((a, b) => b.rank - a.rank || a.index - b.index)[0]?.key ?? null;
90
+ }
91
+ /** Shared observable state for local and embedded clients; it owns no frame or identity authority. */
92
+ export class UiCompositionModel {
93
+ path;
94
+ defaults;
95
+ chrome = INITIAL_CHROME;
96
+ navigation = INITIAL_NAVIGATION;
97
+ chromeListeners = new Set();
98
+ navigationListeners = new Set();
99
+ hints = parseChromeHints(captureChromeHints());
100
+ overrides = {};
101
+ hostRevision = -1;
102
+ hostGeneration = "";
103
+ rootDisabled = false;
104
+ capabilities = [];
105
+ environment;
106
+ router;
107
+ stopRouter;
108
+ endpoint;
109
+ epoch = 0;
110
+ disposed = false;
111
+ refreshing = null;
112
+ constructor(path, mode, defaults = {}) {
113
+ this.path = path;
114
+ this.defaults = defaults;
115
+ this.environment = { mode, connection: "connecting" };
116
+ if (mode === "standalone")
117
+ this.capabilities = [UI_COMPOSITION_CAPABILITY];
118
+ this.publishChrome();
119
+ }
120
+ getChrome = () => this.chrome;
121
+ subscribeChrome = (listener) => { this.chromeListeners.add(listener); return () => { this.chromeListeners.delete(listener); }; };
122
+ getNavigation = () => this.navigation;
123
+ subscribeNavigation = (listener) => { this.navigationListeners.add(listener); return () => { this.navigationListeners.delete(listener); }; };
124
+ configure(defaults) {
125
+ if (!validChromeOverrides(defaults))
126
+ throw new Error("Invalid chrome defaults.");
127
+ this.defaults = structuredClone(defaults);
128
+ this.publishChrome();
129
+ }
130
+ setEnvironment(environment) { this.environment = environment; this.publishChrome(); }
131
+ setHostPolicy(generation, revision, policy, rootDisabled, endpoint) {
132
+ if (!validChromeOverrides(policy) || !Number.isSafeInteger(revision) || revision < 0)
133
+ return;
134
+ if (generation === this.hostGeneration && revision <= this.hostRevision)
135
+ return;
136
+ this.hostGeneration = generation;
137
+ this.hostRevision = revision;
138
+ this.overrides = structuredClone(policy);
139
+ this.rootDisabled = rootDisabled;
140
+ this.endpoint = endpoint;
141
+ this.capabilities = [UI_COMPOSITION_CAPABILITY];
142
+ this.publishChrome();
143
+ this.updateRoute();
144
+ }
145
+ setLegacyHost() {
146
+ this.capabilities = [];
147
+ this.overrides = {};
148
+ this.hostRevision = -1;
149
+ this.hostGeneration = "";
150
+ this.rootDisabled = false;
151
+ this.publishChrome();
152
+ }
153
+ publishChrome() {
154
+ const { navigation, enabled, launcher, sources } = resolveChrome(this.defaults, this.hints, this.overrides);
155
+ const connected = this.environment.connection === "connected";
156
+ this.chrome = Object.freeze({ navigation, tools: Object.freeze({ enabled: enabled && !this.rootDisabled, launcher,
157
+ owner: this.environment.mode === "standalone" ? "local" : connected ? "host" : "unavailable" }),
158
+ revision: this.chrome.revision + 1, hostRevision: this.hostRevision, rootDisabled: this.rootDisabled,
159
+ stale: this.hostRevision >= 0 && !connected, connection: this.environment.connection,
160
+ capabilities: Object.freeze([...this.capabilities]), sources: Object.freeze({ ...sources, ...(this.rootDisabled ? { enabled: "host" } : {}) }) });
161
+ if (typeof document !== "undefined")
162
+ document.documentElement.setAttribute(navigationAttribute, navigation);
163
+ for (const listener of this.chromeListeners)
164
+ listener();
165
+ }
166
+ attachRouter(router) {
167
+ this.stopRouter?.();
168
+ this.router = router;
169
+ if (router)
170
+ this.stopRouter = router.subscribe(() => this.updateRoute());
171
+ else if (typeof window !== "undefined") {
172
+ const changed = () => this.updateRoute();
173
+ window.addEventListener("popstate", changed);
174
+ window.addEventListener("hashchange", changed);
175
+ this.stopRouter = () => { window.removeEventListener("popstate", changed); window.removeEventListener("hashchange", changed); };
176
+ }
177
+ this.updateRoute();
178
+ }
179
+ updateNavigation(patch) {
180
+ this.navigation = Object.freeze({ ...this.navigation, ...patch });
181
+ for (const listener of this.navigationListeners)
182
+ listener();
183
+ }
184
+ currentRoute = () => this.router?.getRoute() ?? `${window.location.pathname}${window.location.search}${window.location.hash}`;
185
+ currentEndpoint = () => {
186
+ const endpoints = new Set(this.navigation.entries.filter(e => e.origin === window.location.origin && e.endpoint).map(e => e.endpoint));
187
+ return this.router?.endpoint ?? this.endpoint ?? (endpoints.size === 1 ? [...endpoints][0] : undefined);
188
+ };
189
+ isLocalEntry(entry) { return entry.origin === window.location.origin && entry.endpoint === this.currentEndpoint(); }
190
+ updateRoute = () => {
191
+ const origins = new Set(this.navigation.entries.filter(e => e.origin === window.location.origin && e.endpoint).map(e => e.endpoint));
192
+ const endpoint = this.router?.endpoint ?? this.endpoint ?? (origins.size === 1 ? [...origins][0] : undefined);
193
+ const route = this.router?.getRoute() ?? `${window.location.pathname}${window.location.search}${window.location.hash}`;
194
+ this.updateNavigation({ activeKey: activeNavigationKey(this.navigation.entries, endpoint, route) });
195
+ };
196
+ refreshNavigation = () => {
197
+ if (this.refreshing)
198
+ return this.refreshing;
199
+ const epoch = this.epoch;
200
+ const operation = (async () => {
201
+ try {
202
+ const catalog = await uiRequest(this.path, "navigation");
203
+ if (this.disposed || epoch !== this.epoch)
204
+ return;
205
+ if (!catalog.installationId || !Array.isArray(catalog.entries) || catalog.entries.some(e => e.route !== null && !isSurfaceRoute(e.route)))
206
+ throw new UiError("ui_response_invalid", "Invalid navigation metadata.");
207
+ const entries = Object.freeze(catalog.entries.map(e => Object.freeze({ ...e })));
208
+ this.updateNavigation({ status: "ready", catalog: Object.freeze({ ...catalog, entries }), entries, code: undefined });
209
+ this.updateRoute();
210
+ }
211
+ catch (error) {
212
+ if (!this.disposed && epoch === this.epoch)
213
+ this.updateNavigation({ status: "unavailable", code: error instanceof UiError ? error.code : "ui_unavailable" });
214
+ }
215
+ })().finally(() => { if (this.refreshing === operation)
216
+ this.refreshing = null; });
217
+ this.refreshing = operation;
218
+ return operation;
219
+ };
220
+ resetNavigation() { this.epoch++; this.refreshing = null; this.updateNavigation({ ...INITIAL_NAVIGATION }); }
221
+ async navigateLocal(entry) {
222
+ if (!entry.route || !isSurfaceRoute(entry.route) || !this.isLocalEntry(entry))
223
+ return false;
224
+ if (this.router)
225
+ await this.router.navigate(entry.route);
226
+ else
227
+ window.location.assign(withChromeHints(new URL(entry.route, window.location.origin).href, this.chrome));
228
+ this.updateRoute();
229
+ return true;
230
+ }
231
+ dispose() { this.disposed = true; this.epoch++; this.stopRouter?.(); this.chromeListeners.clear(); this.navigationListeners.clear(); }
232
+ }
@@ -1,2 +1,4 @@
1
1
  import type { UiHost } from "./ui-host.js";
2
2
  export declare const UiHostContext: import("react").Context<UiHost | null>;
3
+ import type { UiCompositionModel } from "./ui-composition.js";
4
+ export declare const UiCompositionContext: import("react").Context<UiCompositionModel | null>;
@@ -1,3 +1,4 @@
1
1
  "use client";
2
2
  import { createContext } from "react";
3
3
  export const UiHostContext = createContext(null);
4
+ export const UiCompositionContext = createContext(null);
@@ -1,8 +1,12 @@
1
+ import { UiCompositionModel } from "./ui-composition.js";
2
+ import { type UiChromeOverrides, type UiActionOptions, type UiActionResult } from "./ui-types.js";
1
3
  import { type UiClient } from "./ui.js";
2
4
  import { type PanelTarget, type PresentationOptions, type PresentationResult, type UiEnvironment, type UiEvent } from "./ui-types.js";
3
5
  /** Direct-child transport. Discovery carries no identity and never creates a fallback host. */
4
6
  export declare class EmbeddedUiClient implements UiClient {
5
7
  private readonly path;
8
+ readonly composition: UiCompositionModel;
9
+ private readonly localActions;
6
10
  private environment;
7
11
  private nonce;
8
12
  private readonly listeners;
@@ -18,8 +22,18 @@ export declare class EmbeddedUiClient implements UiClient {
18
22
  private readonly onMessage;
19
23
  private readonly onFocus;
20
24
  private readonly onKey;
21
- constructor(path: string);
25
+ constructor(path: string, chrome?: UiChromeOverrides);
22
26
  getEnvironment: () => UiEnvironment;
27
+ getChrome: () => import("./ui-types").UiChromeSnapshot;
28
+ subscribeChrome: (listener: () => void) => () => void;
29
+ getNavigation: () => Readonly<{
30
+ status: "loading" | "ready" | "unavailable";
31
+ catalog: import("./ui-types").UiNavigationCatalog | null;
32
+ entries: readonly import("./ui-types").UiNavigationEntry[];
33
+ activeKey: string | null;
34
+ code?: string;
35
+ }>;
36
+ subscribeNavigation: (listener: () => void) => () => void;
23
37
  setActive(active: boolean): void;
24
38
  subscribe: (listener: (event: UiEvent) => void) => () => void;
25
39
  private emit;
@@ -36,6 +50,9 @@ export declare class EmbeddedUiClient implements UiClient {
36
50
  getPresentation: (requestId: string, options?: {
37
51
  signal?: AbortSignal;
38
52
  }) => Promise<PresentationResult>;
53
+ openToolPicker: (options: UiActionOptions) => Promise<UiActionResult>;
54
+ navigate: (entryKey: string, options: UiActionOptions) => Promise<UiActionResult>;
55
+ private reportRoute;
39
56
  reportSurfaceState: (state: {
40
57
  route: string;
41
58
  state: "ready" | "blocked";