@hosty-sdk/app 0.29.0 → 0.30.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.
@@ -0,0 +1,2 @@
1
+ /** Remove consumed launch parameters immediately, then synchronize routers installed by parent effects. */
2
+ export declare function replaceConsumedUrl(url: URL): void;
@@ -0,0 +1,14 @@
1
+ /** Remove consumed launch parameters immediately, then synchronize routers installed by parent effects. */
2
+ export function replaceConsumedUrl(url) {
3
+ const browser = window;
4
+ const route = `${url.pathname}${url.search}${url.hash}`;
5
+ browser.history.replaceState(null, "", route);
6
+ // Next installs its History API adapter in a parent passive effect, after the SDK's child
7
+ // effects. Repeat the final cleanup once that effect flush ends so its canonical URL also
8
+ // changes. Otherwise HMR restores the original, already-consumed authorization callback.
9
+ // A later cleanup or navigation owns its own URL; never replace it with this older snapshot.
10
+ queueMicrotask(() => {
11
+ if (browser.location.href === url.href)
12
+ browser.history.replaceState(browser.history.state, "", route);
13
+ });
14
+ }
package/dist/react.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { captureChromeHints } from "./ui-composition.js";
4
+ import { replaceConsumedUrl } from "./consumed-url.js";
4
5
  // React slice: the client identity bridge — probe, silent recovery, and the fallback
5
6
  // cards. Prop-less by design: the app id and browser-reachable Core origin arrive in the
6
7
  // probe response's `recovery` field (request-time values, never build-time props).
@@ -161,7 +162,7 @@ export function HostLaunchBridge() {
161
162
  // ignored must not survive into a link someone copies.
162
163
  if (url.searchParams.has(LAUNCH_MODE_PARAM)) {
163
164
  url.searchParams.delete(LAUNCH_MODE_PARAM);
164
- window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
165
+ replaceConsumedUrl(url);
165
166
  }
166
167
  }, []);
167
168
  return null;
@@ -403,7 +404,7 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
403
404
  url.searchParams.delete("state");
404
405
  if (isSilentError || protocolRequired)
405
406
  url.searchParams.delete("error");
406
- window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
407
+ replaceConsumedUrl(url);
407
408
  }
408
409
  if (code && !error && !upgradedLegacy && attempt && !exchangeRef.current) {
409
410
  exchangeRef.current = {
@@ -550,7 +551,7 @@ export function HostThemeBridge({ followSystem = true, onTheme } = {}) {
550
551
  if (url.searchParams.has(THEME_PARAM) || url.searchParams.has(THEME_PREFERENCE_PARAM)) {
551
552
  url.searchParams.delete(THEME_PARAM);
552
553
  url.searchParams.delete(THEME_PREFERENCE_PARAM);
553
- window.history.replaceState(null, "", `${url.pathname}${url.search}${url.hash}`);
554
+ replaceConsumedUrl(url);
554
555
  }
555
556
  const handleMessage = (event) => {
556
557
  const message = parseShellThemeMessage(event, window);
@@ -15,6 +15,7 @@ function link(value) {
15
15
  }
16
16
  /** Shared operator panel; each host supplies its authenticated same-origin Core transport. */
17
17
  export function CoreEnvironmentPanel({ request, storageKey, workspace, workspaceHref }) {
18
+ const [consents, setConsents] = useState([]);
18
19
  const [environments, setEnvironments] = useState([]);
19
20
  const [apps, setApps] = useState({});
20
21
  const [loading, setLoading] = useState(true);
@@ -45,6 +46,7 @@ export function CoreEnvironmentPanel({ request, storageKey, workspace, workspace
45
46
  if (signal?.aborted || !active.current || sequence !== refreshSequence.current)
46
47
  return;
47
48
  const selected = result.environments.filter(e => !workspace || e.context?.workspaceId === workspace.id);
49
+ setConsents(result.consents ?? []);
48
50
  setEnvironments(selected.filter(e => e.state !== "finished"));
49
51
  setLoading(false);
50
52
  setReadError("");
@@ -77,6 +79,7 @@ export function CoreEnvironmentPanel({ request, storageKey, workspace, workspace
77
79
  catch (cause) {
78
80
  if (!signal?.aborted && active.current && sequence === refreshSequence.current) {
79
81
  setEnvironments([]);
82
+ setConsents([]);
80
83
  setApps({});
81
84
  setLoading(false);
82
85
  setReadError(message(cause));
@@ -158,7 +161,7 @@ export function CoreEnvironmentPanel({ request, storageKey, workspace, workspace
158
161
  }
159
162
  }
160
163
  const disabled = busy || pending !== null;
161
- return _jsxs("section", { className: "space-y-4 rounded-lg border bg-card p-4", "aria-label": "Sandbox environments", children: [_jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "font-semibold", children: "Sandbox environments" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Separate Core instances with their own apps, users and temporary data." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { className: button, onClick: () => { setError(""); void refresh(); }, children: "Refresh sandboxes" }), workspace && _jsx("button", { className: button, disabled: disabled || !workspace.worktrees.some(t => t.state === "active"), onClick: () => setCreating(!creating), children: "Create sandbox" })] })] }), creating && workspace && _jsx(CreateEnvironment, { workspace: workspace, disabled: disabled, onCreate: create, onCancel: () => setCreating(false) }), notice && _jsx("p", { role: "status", className: "text-sm text-muted-foreground", children: notice }), (error || readError) && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: error || readError }), pending && _jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsx("span", { children: busy ? `Sandbox ${pending.kind} is in progress…` : "The request outcome is not confirmed." }), _jsx("button", { className: button, disabled: busy, onClick: () => void run(pending), children: "Retry same request" })] }), loading ? _jsx("p", { role: "status", className: "text-sm text-muted-foreground", children: "Loading sandboxes\u2026" }) : !environments.length && !error && !readError && _jsx("p", { className: "text-sm text-muted-foreground", children: "No active sandbox environments." }), environments.map(item => _jsxs("article", { className: "space-y-3 rounded-md border p-3", "aria-label": `Sandbox ${item.label}`, children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-medium", children: [item.label, " ", _jsx("span", { className: "ml-2 rounded border px-2 py-0.5 text-xs font-normal", children: item.state })] }), _jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [item.mode === "source" ? "Core built from workspace source" : item.mode === "release" ? "Installed Core release" : "Current Core build", item.context?.workspaceId && workspaceHref && _jsxs(_Fragment, { children: [" \u00B7 ", _jsx("a", { className: "underline", href: workspaceHref(item.context.workspaceId), children: "Open workspace" })] })] })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [item.state === "running" && link(item.previewUrl) && _jsx("a", { className: button, href: item.previewUrl + "/login", target: "_blank", rel: "noreferrer", children: "Open Core" }), _jsx("button", { className: button, disabled: disabled || ["building", "starting", "stopping", "finishing", "cleanup-failed"].includes(item.state), onClick: () => void command(item, item.state === "running" ? "stop" : "start"), children: item.state === "running" ? "Stop" : "Start" }), _jsx("button", { className: button + " text-destructive", disabled: disabled || item.state === "finishing", onClick: () => setRemoving(item.id), children: item.state === "cleanup-failed" ? "Retry deletion" : "Delete sandbox" })] })] }), item.limits && _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Reserved: ", (item.limits.memoryBytes / 1073741824).toFixed(1), " GiB container memory \u00B7 ", item.limits.cpus, " CPUs \u00B7 ", item.limits.pids, " PIDs \u00B7 ", (item.limits.diskBytes / 1073741824).toFixed(1), " GiB soft disk limit. Released after deletion."] }), item.usage && _jsx("p", { className: "text-xs text-muted-foreground", children: item.usage.state === "unknown" ? "Resource usage unavailable; the reservation remains active." : `Observed: ${(item.usage.containerMemoryBytes / 1048576).toFixed(0)} MiB containers · ${(item.usage.coreMemoryBytes / 1048576).toFixed(0)} MiB Core process · ${(item.usage.ownedDiskBytes / 1073741824).toFixed(2)} GiB owned data.` }), item.error && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: item.error }), item.state === "running" && _jsxs(_Fragment, { children: [_jsx(EnvironmentApplications, { value: apps[item.id] }), apps[item.id] && ["setup-required", "sign-in-required"].includes(apps[item.id].state) && _jsx(SetupAccount, { request: request, id: item.id, disabled: disabled, onDone: () => refresh() })] }), removing === item.id && _jsxs("div", { role: "alertdialog", "aria-label": `Delete ${item.label}`, className: "space-y-3 rounded-md border border-destructive p-3", children: [_jsx("p", { className: "text-sm", children: "Permanently delete this sandbox, all its apps, test users, data and caches? Workspace source and shared folders are preserved." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { className: button + " text-destructive", disabled: disabled, onClick: () => { setRemoving(null); void command(item, "finish"); }, children: "Delete permanently" }), _jsx("button", { className: button, onClick: () => setRemoving(null), children: "Cancel" })] })] })] }, item.id))] });
164
+ return _jsxs("section", { className: "space-y-4 rounded-lg border bg-card p-4", "aria-label": "Sandbox environments", children: [_jsxs("header", { className: "flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { children: [_jsx("h3", { className: "font-semibold", children: "Sandbox environments" }), _jsx("p", { className: "text-sm text-muted-foreground", children: "Separate Core instances with their own apps, users and temporary data." })] }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { className: button, onClick: () => { setError(""); void refresh(); }, children: "Refresh sandboxes" }), workspace && _jsx("button", { className: button, disabled: disabled || !workspace.worktrees.some(t => t.state === "active"), onClick: () => setCreating(!creating), children: "Create sandbox" })] })] }), creating && workspace && _jsx(CreateEnvironment, { workspace: workspace, disabled: disabled, onCreate: create, onCancel: () => setCreating(false) }), notice && _jsx("p", { role: "status", className: "text-sm text-muted-foreground", children: notice }), (error || readError) && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: error || readError }), pending && _jsxs("div", { className: "flex items-center gap-3 text-sm", children: [_jsx("span", { children: busy ? `Sandbox ${pending.kind} is in progress…` : "The request outcome is not confirmed." }), _jsx("button", { className: button, disabled: busy, onClick: () => void run(pending), children: "Retry same request" })] }), loading ? _jsx("p", { role: "status", className: "text-sm text-muted-foreground", children: "Loading sandboxes\u2026" }) : !environments.length && !error && !readError && _jsx("p", { className: "text-sm text-muted-foreground", children: "No active sandbox environments." }), !workspace && consents.length > 0 && _jsxs("details", { className: "rounded-md border p-3", children: [_jsxs("summary", { className: "cursor-pointer text-sm font-medium", children: ["Saved launch permissions (", consents.length, ")"] }), _jsx("p", { className: "mt-2 text-xs text-muted-foreground", children: "Revocation affects all matching launches and delegated access. Running children and their own accounts remain; use Stop or Delete sandbox to end them." }), _jsx("ul", { className: "divide-y", children: consents.map(grant => _jsxs("li", { className: "space-y-2 py-3 text-sm", children: [_jsxs("p", { children: [grant.appId, " \u00B7 ", grant.mode, " \u00B7 ", grant.state] }), _jsx("p", { className: "break-all text-xs text-muted-foreground", children: grant.target }), _jsxs("button", { className: button, disabled: disabled, onClick: () => { setBusy(true); void request(`/consents/${grant.id}/revoke`, {}).then(() => refresh()).catch(cause => setError(message(cause))).finally(() => setBusy(false)); }, children: ["Revoke permission for ", grant.appId] })] }, grant.id)) })] }), environments.map(item => _jsxs("article", { className: "space-y-3 rounded-md border p-3", "aria-label": `Sandbox ${item.label}`, children: [_jsxs("header", { className: "flex flex-wrap items-start justify-between gap-3", children: [_jsxs("div", { children: [_jsxs("h4", { className: "font-medium", children: [item.label, " ", _jsx("span", { className: "ml-2 rounded border px-2 py-0.5 text-xs font-normal", children: item.state })] }), _jsxs("p", { className: "mt-1 text-xs text-muted-foreground", children: [item.mode === "source" ? "Core built from workspace source" : item.mode === "release" ? "Installed Core release" : "Current Core build", item.context?.workspaceId && workspaceHref && _jsxs(_Fragment, { children: [" \u00B7 ", _jsx("a", { className: "underline", href: workspaceHref(item.context.workspaceId), children: "Open workspace" })] })] })] }), _jsxs("div", { className: "flex flex-wrap gap-2", children: [item.state === "running" && link(item.previewUrl) && _jsx("a", { className: button, href: item.previewUrl + "/login", target: "_blank", rel: "noreferrer", children: "Open Core" }), _jsx("button", { className: button, disabled: disabled || ["building", "starting", "stopping", "finishing", "cleanup-failed"].includes(item.state), onClick: () => void command(item, item.state === "running" ? "stop" : "start"), children: item.state === "running" ? "Stop" : "Start" }), _jsx("button", { className: button + " text-destructive", disabled: disabled || item.state === "finishing", onClick: () => setRemoving(item.id), children: item.state === "cleanup-failed" ? "Retry deletion" : "Delete sandbox" })] })] }), item.limits && _jsxs("p", { className: "text-xs text-muted-foreground", children: ["Reserved: ", (item.limits.memoryBytes / 1073741824).toFixed(1), " GiB container memory \u00B7 ", item.limits.cpus, " CPUs \u00B7 ", item.limits.pids, " PIDs \u00B7 ", (item.limits.diskBytes / 1073741824).toFixed(1), " GiB soft disk limit. Released after deletion."] }), item.usage && _jsx("p", { className: "text-xs text-muted-foreground", children: item.usage.state === "unknown" ? "Resource usage unavailable; the reservation remains active." : `Observed: ${(item.usage.containerMemoryBytes / 1048576).toFixed(0)} MiB containers · ${(item.usage.coreMemoryBytes / 1048576).toFixed(0)} MiB Core process · ${(item.usage.ownedDiskBytes / 1073741824).toFixed(2)} GiB owned data.` }), item.access && _jsxs("div", { className: "space-y-2 text-sm", "aria-label": "Sandbox consent", children: [_jsx("p", { children: item.access.state === "operator" ? "Operator launch" : item.access.state === "active" ? item.access.saved ? "Saved launch permission" : "Permission for this run" : item.access.state === "target-changed" ? "Core target changed or unavailable; review launch permission" : "Launch permission revoked or unavailable" }), _jsx("p", { className: "text-xs text-muted-foreground", children: item.access.description }), item.access.grantId && ["active", "target-changed"].includes(item.access.state) && _jsx("button", { className: button, disabled: disabled, onClick: () => { setBusy(true); void request(`/${item.id}/consent/revoke`, {}).then(() => refresh()).catch(cause => setError(message(cause))).finally(() => setBusy(false)); }, children: "Revoke launch permission" })] }), item.error && _jsx("p", { role: "alert", className: "text-sm text-destructive", children: item.error }), item.state === "running" && _jsxs(_Fragment, { children: [_jsx(EnvironmentApplications, { value: apps[item.id] }), apps[item.id] && ["setup-required", "sign-in-required"].includes(apps[item.id].state) && _jsx(SetupAccount, { request: request, id: item.id, disabled: disabled, onDone: () => refresh() })] }), removing === item.id && _jsxs("div", { role: "alertdialog", "aria-label": `Delete ${item.label}`, className: "space-y-3 rounded-md border border-destructive p-3", children: [_jsx("p", { className: "text-sm", children: "Permanently delete this sandbox, all its apps, test users, data and caches? Workspace source and shared folders are preserved." }), _jsxs("div", { className: "flex gap-2", children: [_jsx("button", { className: button + " text-destructive", disabled: disabled, onClick: () => { setRemoving(null); void command(item, "finish"); }, children: "Delete permanently" }), _jsx("button", { className: button, onClick: () => setRemoving(null), children: "Cancel" })] })] })] }, item.id))] });
162
165
  }
163
166
  function EnvironmentApplications({ value }) {
164
167
  if (!value)
@@ -131,8 +131,16 @@ export declare class SandboxClient {
131
131
  }): Promise<SandboxInstance>;
132
132
  call<T = unknown>(action: SandboxAction, input: Record<string, unknown>): Promise<T>;
133
133
  }
134
- export declare const coreEnvironmentActions: readonly ["environment-create", "environment-list", "environment-status", "environment-start", "environment-stop", "environment-restart", "environment-finish", "environment-setup", "environment-api"];
134
+ export declare const coreEnvironmentActions: readonly ["environment-consent", "environment-create", "environment-list", "environment-status", "environment-start", "environment-stop", "environment-restart", "environment-finish", "environment-setup", "environment-api"];
135
135
  export type CoreEnvironmentAction = typeof coreEnvironmentActions[number];
136
+ export interface CoreEnvironmentSavedConsent {
137
+ id: string;
138
+ appId: string;
139
+ label: string;
140
+ mode: string;
141
+ target: string;
142
+ state: string;
143
+ }
136
144
  export interface CoreEnvironment {
137
145
  id: string;
138
146
  label: string;
@@ -159,6 +167,12 @@ export interface CoreEnvironment {
159
167
  applicationCpus: number;
160
168
  applicationPids: number;
161
169
  };
170
+ access?: {
171
+ state: string;
172
+ grantId?: string;
173
+ saved: boolean;
174
+ description: string;
175
+ };
162
176
  usage?: {
163
177
  ownedDiskBytes: number;
164
178
  containerMemoryBytes: number;
@@ -93,7 +93,7 @@ export class SandboxClient {
93
93
  return result;
94
94
  }
95
95
  }
96
- export const coreEnvironmentActions = ["environment-create", "environment-list", "environment-status", "environment-start",
96
+ export const coreEnvironmentActions = ["environment-consent", "environment-create", "environment-list", "environment-status", "environment-start",
97
97
  "environment-stop", "environment-restart", "environment-finish", "environment-setup", "environment-api"];
98
98
  /** Full child Core transport. Requires core.environments permissions and live administrator authority.
99
99
  * Setup uses ordinary password authentication. Parent credentials are never forwarded to the child. */
@@ -116,8 +116,8 @@ export class CoreEnvironmentClient {
116
116
  throw new SandboxError("environment_operation_invalid", "Unknown environment operation.", 400);
117
117
  const { environmentId, ...body } = input;
118
118
  const read = action === "environment-list" || action === "environment-status";
119
- let suffix = "";
120
- if (action !== "environment-list" && action !== "environment-create") {
119
+ let suffix = action === "environment-consent" ? "/consent" : "";
120
+ if (action !== "environment-list" && action !== "environment-create" && action !== "environment-consent") {
121
121
  if (typeof environmentId !== "string" || !/^[a-f0-9]{32}$/.test(environmentId))
122
122
  throw new SandboxError("environment_id_invalid", "Select a Core-issued environment ID.", 400);
123
123
  suffix = `/${environmentId}` + (action === "environment-status" ? "" : action === "environment-api" ? "/api"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.29.0",
3
+ "version": "0.30.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",