@hosty-sdk/app 0.12.0 → 0.13.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/react.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { ReactNode } from "react";
1
2
  import { type AppLaunchMode, type AppSessionStatus } from "./index.js";
2
3
  import { type HostyResolvedTheme, type HostyThemePreference } from "./theme.js";
3
4
  /**
@@ -25,11 +26,28 @@ export declare function HostLaunchBridge(): null;
25
26
  * express the decision.
26
27
  */
27
28
  export declare function useLaunchMode(): AppLaunchMode | null;
29
+ export type AppIdentityBridgeState = {
30
+ kind: "recovering";
31
+ } | {
32
+ kind: "active";
33
+ } | {
34
+ kind: "signin";
35
+ openUrl: string | null;
36
+ embedded: boolean;
37
+ } | {
38
+ kind: "denied";
39
+ } | {
40
+ kind: "unavailable";
41
+ } | {
42
+ kind: "misconfigured";
43
+ };
28
44
  export interface AppIdentityBridgeProps {
29
45
  /** Probe endpoint returning a session status + recovery params. */
30
46
  probePath?: string;
31
47
  /** Code-exchange endpoint. */
32
48
  appCodePath?: string;
49
+ /** Optional app-owned rendering, including loading and active content. Recovery remains SDK-owned. */
50
+ renderState?: (state: AppIdentityBridgeState) => ReactNode;
33
51
  }
34
52
  /**
35
53
  * Mount once at the top of the root layout body. On first load it consumes a `?code` from
@@ -38,7 +56,7 @@ export interface AppIdentityBridgeProps {
38
56
  * redirect through Core `/open` when standalone, and cards only in fallback/terminal states
39
57
  * — never a login UI while recovery is still running.
40
58
  */
41
- export declare function AppIdentityBridge({ probePath, appCodePath, }?: AppIdentityBridgeProps): import("react").JSX.Element | null;
59
+ export declare function AppIdentityBridge({ probePath, appCodePath, renderState, }?: AppIdentityBridgeProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
42
60
  export interface HostThemeBridgeProps {
43
61
  /**
44
62
  * Whether the operating system decides the theme while no shell has spoken — `true` unless the
package/dist/react.js CHANGED
@@ -148,8 +148,11 @@ export function useLaunchMode() {
148
148
  * redirect through Core `/open` when standalone, and cards only in fallback/terminal states
149
149
  * — never a login UI while recovery is still running.
150
150
  */
151
- export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePath = "/api/auth/app-code", } = {}) {
152
- const [ui, setUi] = useState({ kind: "hidden" });
151
+ export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePath = "/api/auth/app-code", renderState, } = {}) {
152
+ const [ui, setUi] = useState({ kind: "recovering" });
153
+ // A launch code is single-use. Strict Mode replays the effect after the URL is cleaned;
154
+ // keep its exchange alive and let the replacement effect await the same response.
155
+ const exchangeRef = useRef(null);
153
156
  useEffect(() => {
154
157
  let cancelled = false;
155
158
  const controller = new AbortController();
@@ -198,7 +201,7 @@ export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePat
198
201
  });
199
202
  switch (action.kind) {
200
203
  case "none":
201
- setUi({ kind: "hidden" });
204
+ setUi({ kind: "active" });
202
205
  return;
203
206
  case "post-auth-required": {
204
207
  // The payload carries no secret, so targetOrigin "*" is safe — the Shell verifies
@@ -235,12 +238,18 @@ export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePat
235
238
  if (code) {
236
239
  url.searchParams.delete("code");
237
240
  window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
238
- void fetch(appCodePath, {
239
- method: "POST",
240
- headers: { "Content-Type": "application/json" },
241
- body: JSON.stringify({ code }),
242
- signal: controller.signal,
243
- })
241
+ exchangeRef.current = {
242
+ path: appCodePath,
243
+ response: fetch(appCodePath, {
244
+ method: "POST",
245
+ headers: { "Content-Type": "application/json" },
246
+ body: JSON.stringify({ code }),
247
+ }),
248
+ };
249
+ }
250
+ const exchange = exchangeRef.current;
251
+ if (exchange && exchange.path === appCodePath) {
252
+ void exchange.response
244
253
  .then((response) => {
245
254
  if (cancelled) {
246
255
  return;
@@ -250,11 +259,13 @@ export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePat
250
259
  window.location.reload();
251
260
  }
252
261
  else {
262
+ exchangeRef.current = null;
253
263
  void probeAndRecover();
254
264
  }
255
265
  })
256
266
  .catch(() => {
257
267
  if (!cancelled) {
268
+ exchangeRef.current = null;
258
269
  void probeAndRecover();
259
270
  }
260
271
  });
@@ -267,7 +278,9 @@ export function AppIdentityBridge({ probePath = "/api/auth/identity", appCodePat
267
278
  controller.abort();
268
279
  };
269
280
  }, [probePath, appCodePath]);
270
- if (ui.kind === "hidden") {
281
+ if (renderState)
282
+ return renderState(ui);
283
+ if (ui.kind === "recovering" || ui.kind === "active") {
271
284
  return null;
272
285
  }
273
286
  return (_jsx("div", { role: "status", style: barStyle, children: ui.kind === "signin" ? (_jsxs(_Fragment, { children: [_jsx("span", { children: "Your Hosty session ended." }), ui.openUrl ? (_jsx("a", { href: ui.openUrl, ...(ui.embedded ? { target: "_blank", rel: "noopener noreferrer" } : {}), style: actionStyle, children: "Sign in via Hosty" })) : (_jsx("span", { children: "Open this app from the machine running Hosty, or configure a public origin." }))] })) : ui.kind === "denied" ? (_jsx("span", { children: "You are signed in to Hosty but are not allowed to use this app." })) : ui.kind === "misconfigured" ? (_jsx("span", { children: "This app is not configured correctly on the host. Contact the administrator." })) : (_jsxs(_Fragment, { children: [_jsx("span", { children: "Can\u2019t reach Hosty right now." }), _jsx("button", { type: "button", onClick: () => window.location.reload(), style: actionStyle, children: "Retry" })] })) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Hosty app auth: session state machine, recovery bridge, Core revalidation, and the embedder responder.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  "server-only": "^0.0.1"
47
47
  },
48
48
  "peerDependencies": {
49
- "next": "^16.3.3",
49
+ "next": "^16.3.4",
50
50
  "react": "^19.2.8"
51
51
  },
52
52
  "peerDependenciesMeta": {
@@ -61,7 +61,7 @@
61
61
  "@types/node": "^24",
62
62
  "@types/react": "^19.2.18",
63
63
  "typescript": "^6",
64
- "vitest": "^4.1.11"
64
+ "vitest": "^5.0.0"
65
65
  },
66
66
  "repository": {
67
67
  "type": "git",