@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 +19 -1
- package/dist/react.js +23 -10
- package/package.json +3 -3
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: "
|
|
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: "
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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": "^
|
|
64
|
+
"vitest": "^5.0.0"
|
|
65
65
|
},
|
|
66
66
|
"repository": {
|
|
67
67
|
"type": "git",
|