@hosty-sdk/app 0.25.12 → 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/README.md CHANGED
@@ -69,8 +69,8 @@ After first mount, blocked content and app portals become hidden and inert while
69
69
  remains mounted. Same-user renewal restores it. A different stable user ID reloads the app document
70
70
  and refuses to replay old pending requests. `APP_SESSION_RESTORED` from `/browser-auth` is an optional
71
71
  DOM notification with `detail.userId`; it contains no credential. There is no generic draft persistence
72
- across reloads or cache-clearing framework. Existing identity idle/absolute lifetimes and fixed
73
- privileged activity windows still apply.
72
+ across reloads or cache-clearing framework. Current user/app permissions and the authorizing principal's idle/absolute lifetime still apply.
73
+ Core 0.130.0 advertises continuous app sessions; older servers retain their actual activity policy.
74
74
 
75
75
  Compatible SDK updates change standard presentation and recovery after a dependency update and
76
76
  rebuild, without editing this wiring. New mandatory Core capabilities can require upgrading Core.
@@ -289,16 +289,39 @@ reads or `mcp:invoke` for assistant mutations, then apply your application's use
289
289
  Do not cache validation. Keep `introspectScopedToken` on ordinary APIs: it does not opt into accepting
290
290
  MCP-only credentials. Neither token permits skipping the application's own authorization.
291
291
 
292
- ## Privileged activity renewal
292
+ ## Continuous app access (Core 0.130.0 / SDK 0.26.0)
293
293
 
294
- Identity probes used by `AppIdentityBridge` pass `activeUntil` and `activityRequired` through from
295
- `resolveAppSession().identity`. Keep app APIs on `appFetch`; a 401 can then request a user-click
296
- Core popup, exchange a code on the same origin and retry once without replacing mounted content.
297
- For another UI framework, use `configureAppActivity` with an app-owned `exchangeCode` function,
298
- listen for `APP_SESSION_ENDED`, and call `renewAppActivity` directly from a click. Do not navigate
299
- or reload to renew access. `appActivityNeedsRenewal()` supports proactive click renewal near expiry.
300
- See [the activity contract](../../docs/features/app-activity-window/feature.md), including the
301
- separate Core-approved assistant session leases.
294
+ Pass `continuity`, `activeUntil` and `activityRequired` from Core revalidation through custom identity
295
+ probes. Standard SDK adapters do this automatically. `HostyOverlay` / `useAppIdentity` maintain a
296
+ live browser parent through a Core-owned frame and refresh the app-local probe. Checks coalesce,
297
+ run on mount/return/network recovery, and repeat at a jittered five-minute interval. App backends
298
+ cannot keep the browser parent alive with app credentials alone.
299
+
300
+ Keep protected client requests on `appFetch`. An outage returns 503 without clearing identity or
301
+ replaying a mutation. Real expiry uses normal recovery, preserving same-user state. Optional
302
+ `<HostyOverlay localContent={<LocalNotes />}>…</HostyOverlay>` places explicitly local/public
303
+ content outside the protected root during an outage; do not place private actor-bound data there.
304
+
305
+ For another framework, use `configureAppContinuity` / `ensureAppContinuity` from
306
+ `@hosty-sdk/app/continuity/browser`, with validated metadata from `/continuity` and a probe that
307
+ checks the same actor. The probe must bypass maintenance (`appFetch(path, init, false)`) to avoid
308
+ recursion. Clean up the coordinator on unmount. `APP_CONTINUITY_UPDATED` reports silent success;
309
+ `APP_ACTIVITY_RENEWED` continues to mean explicit recovery. Do not replay handoffs/messages on either.
310
+ Missing continuity metadata keeps older Core's `configureAppActivity`/click renewal behavior.
311
+
312
+ Server-only unattended work requires an explicit Core grant:
313
+
314
+ ```ts
315
+ import { BackgroundGrantClient } from "@hosty-sdk/app/background/server";
316
+ const background = new BackgroundGrantClient(grantId, "health"); // HOSTY_* service configuration
317
+ const response = await background.core("/api/apps/example.target/health");
318
+ ```
319
+
320
+ The client coalesces credential issuance and refreshes before expiry. It sends each operation once;
321
+ Core enforces the immutable selected scope online. `token()` can supply the existing provider/MCP
322
+ client's user-token input. Keep tokens server-side. See
323
+ [Continuous App Access](../../docs/features/continuous-app-access/feature.md) for creation, logout,
324
+ revocation, exact MCP tools and installation boundaries. Background grants do not create a scheduler.
302
325
 
303
326
  ## Shared panels (Core 0.129.0 / SDK 0.25.0)
304
327
 
@@ -0,0 +1,16 @@
1
+ import type { ProviderServerOptions } from "@hosty-sdk/app/providers/server";
2
+ /** An explicit Core delegation reference. Keep this client and its credentials on the server. */
3
+ export declare class BackgroundGrantClient {
4
+ private readonly grantId;
5
+ private readonly scopeId;
6
+ private credential?;
7
+ private pending?;
8
+ private readonly origin;
9
+ private readonly appId;
10
+ private readonly service;
11
+ constructor(grantId: string, scopeId: string, options?: ProviderServerOptions);
12
+ /** Coalesces issuance; each protected dispatch still validates the delegation online in Core. */
13
+ token(): Promise<string>;
14
+ /** Sends one operation. No automatic replay of a possibly completed mutation. */
15
+ core(path: string, init?: RequestInit): Promise<Response>;
16
+ }
@@ -0,0 +1,52 @@
1
+ import { ProviderError } from "@hosty-sdk/app/providers";
2
+ /** An explicit Core delegation reference. Keep this client and its credentials on the server. */
3
+ export class BackgroundGrantClient {
4
+ grantId;
5
+ scopeId;
6
+ credential;
7
+ pending;
8
+ origin;
9
+ appId;
10
+ service;
11
+ constructor(grantId, scopeId, options = {}) {
12
+ this.grantId = grantId;
13
+ this.scopeId = scopeId;
14
+ this.origin = new URL(options.coreOrigin ?? process.env.HOSTY_CORE_ORIGIN ?? "http://127.0.0.1:7070");
15
+ this.appId = options.appId ?? process.env.HOSTY_APP_ID ?? "";
16
+ this.service = options.serviceToken ?? process.env.HOSTY_APP_SERVICE_TOKEN ?? "";
17
+ if (!this.appId || !this.service || !grantId || !scopeId)
18
+ throw new Error("An app service identity and explicit background grant/scope references are required.");
19
+ }
20
+ /** Coalesces issuance; each protected dispatch still validates the delegation online in Core. */
21
+ token() {
22
+ if (this.credential && Date.parse(this.credential.expiresAt) > Date.now() + 30_000)
23
+ return Promise.resolve(this.credential.accessToken);
24
+ if (this.pending)
25
+ return this.pending;
26
+ this.pending = (async () => {
27
+ const response = await fetch(new URL(`/api/internal/apps/${encodeURIComponent(this.appId)}/background/token`, this.origin), {
28
+ method: "POST", redirect: "error", cache: "no-store", signal: AbortSignal.timeout(10_000),
29
+ headers: { authorization: `Bearer ${this.service}`, "content-type": "application/json" },
30
+ body: JSON.stringify({ grantId: this.grantId, scopeId: this.scopeId }),
31
+ });
32
+ const body = await response.json();
33
+ if (!response.ok)
34
+ throw new ProviderError(body.code ?? "background_denied", body.message ?? "Background access is unavailable.", response.status);
35
+ if (typeof body.accessToken !== "string" || !Number.isFinite(Date.parse(body.expiresAt)))
36
+ throw new Error("Invalid background credential response.");
37
+ this.credential = body;
38
+ return body.accessToken;
39
+ })().finally(() => { this.pending = undefined; });
40
+ return this.pending;
41
+ }
42
+ /** Sends one operation. No automatic replay of a possibly completed mutation. */
43
+ async core(path, init = {}) {
44
+ const url = new URL(path, this.origin);
45
+ if (url.origin !== this.origin.origin || !url.pathname.startsWith("/api/apps/") || url.username || url.password)
46
+ throw new Error("Use a Core app operation path.");
47
+ const headers = new Headers(init.headers);
48
+ headers.set("authorization", `Bearer ${this.service}`);
49
+ headers.set("X-Hosty-App-Identity", await this.token());
50
+ return fetch(url, { ...init, headers, redirect: "error", cache: "no-store" });
51
+ }
52
+ }
@@ -1,3 +1,5 @@
1
+ export { APP_CONTINUITY_UPDATED } from "@hosty-sdk/app/continuity/browser";
2
+ export { readAppContinuity, type AppContinuity } from "@hosty-sdk/app/continuity";
1
3
  import type { AppAuthProtocol } from "./app-code.js";
2
4
  export type AppSignInResult = {
3
5
  code: string;
@@ -1,3 +1,6 @@
1
+ import { ensureAppContinuity } from "@hosty-sdk/app/continuity/browser";
2
+ export { APP_CONTINUITY_UPDATED } from "@hosty-sdk/app/continuity/browser";
3
+ export { readAppContinuity } from "@hosty-sdk/app/continuity";
1
4
  import { sha256 } from "@noble/hashes/sha2.js";
2
5
  function isValidCodeVerifier(value) {
3
6
  return typeof value === "string" && /^[A-Za-z0-9._~-]{43,128}$/.test(value);
@@ -270,6 +273,8 @@ export async function appFetch(input, init = {}, recover = true) {
270
273
  // SDK identity/protocol probes use recover=false so that validation itself can proceed.
271
274
  if (recover && renewal && activityConfig?.validateSession && !await waitForActivity(init.signal))
272
275
  return Response.json({ code: "reauth_required", message: "Hosty access has not been restored." }, { status: 401 });
276
+ if (recover && !await ensureAppContinuity())
277
+ return Response.json({ code: "session_unavailable", message: "Hosty access could not be verified." }, { status: 503 });
273
278
  const current = grant?.origin === url.origin ? grant : null;
274
279
  const headers = new Headers(init.headers);
275
280
  if (current)
@@ -0,0 +1,11 @@
1
+ import type { AppContinuity } from "./continuity.js";
2
+ export declare const APP_CONTINUITY_UPDATED = "hosty:app-continuity-updated";
3
+ type Owner = {
4
+ coreOrigin: string;
5
+ authority: AppContinuity;
6
+ probe: () => Promise<boolean>;
7
+ };
8
+ /** One coordinator per app document; probes bypass appFetch's maintenance to avoid recursion. */
9
+ export declare function ensureAppContinuity(force?: boolean): Promise<boolean>;
10
+ export declare function configureAppContinuity(config: Owner): () => void;
11
+ export {};
@@ -0,0 +1,92 @@
1
+ export const APP_CONTINUITY_UPDATED = "hosty:app-continuity-updated";
2
+ let owner = null;
3
+ let pending = null;
4
+ let checkedAt = 0;
5
+ let valid = true;
6
+ /** One coordinator per app document; probes bypass appFetch's maintenance to avoid recursion. */
7
+ export async function ensureAppContinuity(force = false) {
8
+ if (!owner)
9
+ return true;
10
+ if (pending)
11
+ return pending;
12
+ if (!force && Date.now() - checkedAt < Math.min(30_000, owner.authority.checkAfterSeconds * 1000))
13
+ return valid;
14
+ const current = owner;
15
+ pending = (async () => {
16
+ // Browser policy may refuse the frame. Only the app's authoritative probe decides access.
17
+ if (current.authority.mode === "browser")
18
+ await maintainParent(current);
19
+ const result = await current.probe().catch(() => false);
20
+ if (owner !== current)
21
+ return false;
22
+ valid = result;
23
+ checkedAt = Date.now();
24
+ if (result)
25
+ window.dispatchEvent(new Event(APP_CONTINUITY_UPDATED));
26
+ return result;
27
+ })().finally(() => { pending = null; });
28
+ return pending;
29
+ }
30
+ export function configureAppContinuity(config) {
31
+ owner = config;
32
+ checkedAt = 0;
33
+ valid = true;
34
+ let disposed = false;
35
+ let failures = 0;
36
+ let scheduleVersion = 0;
37
+ let timer;
38
+ const schedule = async (force = false) => {
39
+ const version = ++scheduleVersion;
40
+ clearTimeout(timer);
41
+ const ok = await ensureAppContinuity(force);
42
+ // Events can share one pending probe; only the latest waiter installs its next timer.
43
+ if (disposed || version !== scheduleVersion)
44
+ return;
45
+ failures = ok ? 0 : Math.min(4, failures + 1);
46
+ const seconds = ok ? config.authority.checkAfterSeconds : Math.min(300, 15 * 2 ** failures);
47
+ timer = setTimeout(() => { void schedule(true); }, seconds * 1000 * (0.9 + Math.random() * 0.2));
48
+ };
49
+ const wake = () => { void schedule(); };
50
+ const online = () => { void schedule(true); };
51
+ window.addEventListener("focus", wake);
52
+ window.addEventListener("online", online);
53
+ document.addEventListener("visibilitychange", wake);
54
+ void schedule(true);
55
+ return () => {
56
+ disposed = true;
57
+ clearTimeout(timer);
58
+ window.removeEventListener("focus", wake);
59
+ window.removeEventListener("online", online);
60
+ document.removeEventListener("visibilitychange", wake);
61
+ if (owner === config) {
62
+ owner = null;
63
+ checkedAt = 0;
64
+ }
65
+ };
66
+ }
67
+ function maintainParent(config) {
68
+ return new Promise(resolve => {
69
+ const state = Array.from(crypto.getRandomValues(new Uint8Array(32)), b => b.toString(16).padStart(2, "0")).join("");
70
+ const url = new URL("/api/auth/apps/continuity/frame", config.coreOrigin);
71
+ url.searchParams.set("grantId", config.authority.grantId);
72
+ url.searchParams.set("origin", window.location.origin);
73
+ url.searchParams.set("state", state);
74
+ const frame = document.createElement("iframe");
75
+ frame.hidden = true;
76
+ frame.title = "Hosty session maintenance";
77
+ frame.referrerPolicy = "no-referrer";
78
+ const done = () => { clearTimeout(timer); window.removeEventListener("message", received); frame.remove(); resolve(); };
79
+ const received = (event) => {
80
+ if (event.source === frame.contentWindow && event.origin === url.origin &&
81
+ event.data?.type === "hosty:continuity-result" && event.data.state === state &&
82
+ ["maintained", "unavailable"].includes(event.data.status))
83
+ done();
84
+ };
85
+ const timer = setTimeout(done, 4_000);
86
+ window.addEventListener("message", received);
87
+ frame.onload = () => frame.contentWindow?.postMessage({ type: "hosty:continuity-check", state }, url.origin);
88
+ frame.onerror = done;
89
+ frame.src = url.href;
90
+ document.body.append(frame);
91
+ });
92
+ }
@@ -0,0 +1,9 @@
1
+ /** Server-advertised authority, never inferred from embedding or a missing activity deadline. */
2
+ export type AppContinuity = {
3
+ version: 1;
4
+ mode: "browser" | "native";
5
+ grantId: string;
6
+ expiresAt: string;
7
+ checkAfterSeconds: number;
8
+ };
9
+ export declare function readAppContinuity(value: unknown): AppContinuity | undefined;
@@ -0,0 +1,9 @@
1
+ export function readAppContinuity(value) {
2
+ const v = value;
3
+ if (v?.version !== 1 || !["browser", "native"].includes(v.mode ?? "") ||
4
+ typeof v.grantId !== "string" || !v.grantId || v.grantId.length > 128 ||
5
+ typeof v.expiresAt !== "string" || !Number.isFinite(Date.parse(v.expiresAt)) ||
6
+ typeof v.checkAfterSeconds !== "number" || !Number.isFinite(v.checkAfterSeconds) || v.checkAfterSeconds <= 0)
7
+ return;
8
+ return { ...v, checkAfterSeconds: Math.min(300, v.checkAfterSeconds) };
9
+ }
@@ -1,6 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
2
  /** Mount once around protected content. Presentation and recovery belong to Hosty. */
3
- export declare function HostyOverlay({ children, probePath, appCodePath, uiPath, panelLayout }: {
3
+ export declare function HostyOverlay({ children, probePath, appCodePath, uiPath, panelLayout, localContent }: {
4
+ /** Explicitly public/local content available during an outage; never put protected actor data here. */
5
+ localContent?: ReactNode;
4
6
  children: ReactNode;
5
7
  probePath?: string;
6
8
  appCodePath?: string;
@@ -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 }) {
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, { 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>;
package/dist/react.js CHANGED
@@ -3,6 +3,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  // React slice: the client identity bridge — probe, silent recovery, and the fallback
4
4
  // cards. Prop-less by design: the app id and browser-reachable Core origin arrive in the
5
5
  // probe response's `recovery` field (request-time values, never build-time props).
6
+ import { readAppContinuity } from "./continuity.js";
7
+ import { configureAppContinuity } from "./continuity-browser.js";
6
8
  import { AppActivityBridge } from "./activity-react.js";
7
9
  import { AuthNotice, AuthNoticeButton, authNoticeActionStyle } from "./auth-notice.js";
8
10
  import { useEffect, useRef, useState } from "react";
@@ -203,6 +205,18 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
203
205
  const exchangeRef = useRef(null);
204
206
  const onSessionRef = useRef(onSession);
205
207
  useEffect(() => { onSessionRef.current = onSession; }, [onSession]);
208
+ const continuity = activity?.continuity;
209
+ const continuityActor = session?.userId;
210
+ useEffect(() => {
211
+ if (!continuity || !activity?.openUrl || !continuityActor)
212
+ return;
213
+ return configureAppContinuity({ coreOrigin: new URL(activity.openUrl).origin, authority: continuity,
214
+ probe: async () => {
215
+ const next = await refreshRef.current();
216
+ return next?.status === "active" && next.userId === continuityActor
217
+ && (!next.hosty || (next.hosty.version === 1 && next.hosty.setup === "ready"));
218
+ } });
219
+ }, [continuity?.grantId, activity?.openUrl, continuityActor]);
206
220
  useEffect(() => {
207
221
  let cancelled = false;
208
222
  let probeSequence = 0;
@@ -263,7 +277,7 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
263
277
  forgetRejectedAppGrant(rejectionCode, revision);
264
278
  if (openUrl && protocol)
265
279
  setActivity({ openUrl, appAuthProtocol: protocol,
266
- activeUntil: activityMetadata?.activeUntil, activityRequired: activityMetadata?.activityRequired });
280
+ activeUntil: activityMetadata?.activeUntil, activityRequired: activityMetadata?.activityRequired, continuity: readAppContinuity(activityMetadata?.continuity) });
267
281
  const initialLoad = firstProbe.current;
268
282
  firstProbe.current = false;
269
283
  if (silentError.current === "access_denied") {
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.25.12",
3
+ "version": "0.26.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",
@@ -104,6 +104,18 @@
104
104
  "./permissions/react": {
105
105
  "types": "./dist/permissions-react.d.ts",
106
106
  "default": "./dist/permissions-react.js"
107
+ },
108
+ "./background/server": {
109
+ "types": "./dist/background-server.d.ts",
110
+ "default": "./dist/background-server.js"
111
+ },
112
+ "./continuity": {
113
+ "types": "./dist/continuity.d.ts",
114
+ "default": "./dist/continuity.js"
115
+ },
116
+ "./continuity/browser": {
117
+ "types": "./dist/continuity-browser.d.ts",
118
+ "default": "./dist/continuity-browser.js"
107
119
  }
108
120
  },
109
121
  "scripts": {