@hosty-sdk/app 0.37.0 → 0.39.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.
@@ -55,3 +55,18 @@ export class TaskExecutorClient {
55
55
  return this.call(`/executions/${encodeURIComponent(executionId)}/cancel`, "POST", { requestId }, signal);
56
56
  }
57
57
  }
58
+ /** Core-mediated child calls retain the original caller and the exact task-provider attribution. */
59
+ export class MediatedTaskExecutorClient {
60
+ providers;
61
+ executor;
62
+ dispatch;
63
+ constructor(providers, executor, dispatch) {
64
+ this.providers = providers;
65
+ this.executor = executor;
66
+ this.dispatch = dispatch;
67
+ }
68
+ options(signal) { return this.providers.taskDispatch({ dispatch: this.dispatch, executor: this.executor, operation: "options" }, signal); }
69
+ get(executionId, signal) { return this.providers.taskDispatch({ dispatch: this.dispatch, executor: this.executor, operation: "get", executionId }, signal); }
70
+ accept(executionId, input, signal) { return this.providers.taskDispatch({ dispatch: this.dispatch, executor: this.executor, operation: "accept", executionId, input }, signal); }
71
+ cancel(executionId, requestId, signal) { return this.providers.taskDispatch({ dispatch: this.dispatch, executor: this.executor, operation: "cancel", executionId, requestId }, signal); }
72
+ }
package/dist/ui-frame.js CHANGED
@@ -137,5 +137,5 @@ export function HostyAppFrame(props) {
137
137
  const insecure = typeof window !== "undefined" && window.location.protocol === "https:" && new URL(surface.origin).protocol === "http:";
138
138
  if (insecure)
139
139
  return _jsxs("div", { "data-hosty-sdk": true, role: "status", className: "hosty-panel-notice", children: [_jsx(SdkChromeStyles, {}), _jsxs("strong", { children: [title, " needs HTTPS to appear here"] }), _jsx("p", { children: "This app publishes an HTTP address. Configure its public HTTPS origin in Hosty." }), /^(localhost|127\.|\[::1\])/.test(new URL(surface.origin).hostname) && _jsx("p", { children: "This local address is reachable only from the Hosty computer." }), standaloneHref && _jsx("a", { href: standaloneHref, target: "_blank", rel: "noopener noreferrer", children: "Open separately" })] });
140
- return _jsxs(_Fragment, { children: [review && _jsxs(_Fragment, { children: [_jsx(SdkChromeStyles, {}), _jsx("a", { "data-hosty-sdk": true, className: "hosty-panel-review", href: review, target: "_blank", rel: "noopener noreferrer", children: "Review access in Core" })] }), _jsx("iframe", { ref: ref, className: className, title: title, src: launch, sandbox: frameSandbox(surface.framePolicy), allow: surface.framePolicy.microphone ? "clipboard-write; microphone 'src'" : "clipboard-write", style: { border: 0, height: "100%", width: "100%", colorScheme: resolvedTheme }, onLoad: () => setLoaded(true) })] });
140
+ return _jsxs(_Fragment, { children: [review && _jsxs(_Fragment, { children: [_jsx(SdkChromeStyles, {}), _jsx("a", { "data-hosty-sdk": true, className: "hosty-panel-review", href: review, target: "_blank", rel: "noopener noreferrer", children: "Review access in Core" })] }), _jsx("iframe", { ref: ref, className: className, title: title, src: launch, sandbox: frameSandbox(surface.framePolicy), allow: surface.framePolicy.microphone ? "clipboard-write; display-capture 'src'; microphone 'src'" : "clipboard-write; display-capture 'src'", style: { border: 0, height: "100%", width: "100%", colorScheme: resolvedTheme }, onLoad: () => setLoaded(true) })] });
141
141
  }
package/dist/ui-react.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { FeedbackCapture } from "./feedback-react.js";
3
4
  import { SdkChromeStyles, SdkIcon } from "./sdk-chrome.js";
4
5
  import { useContext, useEffect, useId, useLayoutEffect, useRef, useState, useSyncExternalStore } from "react";
5
6
  import { bindUiClient, ui } from "./ui.js";
@@ -53,7 +54,7 @@ export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = t
53
54
  if (allowed)
54
55
  mounted.current = true;
55
56
  const content = _jsxs("div", { className: "hosty-app-layout", children: [navigation?.renderer !== "custom" && _jsx(HostyNavigation, {}), _jsxs("div", { className: "hosty-app-content", "data-hosty-content": true, tabIndex: -1, children: [_jsx(HostyToolLauncher, {}), children] })] });
56
- return _jsx(UiHostContext.Provider, { value: host, children: _jsx(UiCompositionContext.Provider, { value: clientRef.current?.composition ?? null, children: _jsxs(UiFrameLayerContext.Provider, { value: layer, children: [_jsx("style", { children: `.hosty-app-layout{display:flex;width:100%;height:100%;min-width:0;min-height:0}.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto;outline:none}` }), _jsx("div", { hidden: !allowed, inert: !allowed, style: { display: allowed ? "contents" : "none" }, children: mounted.current && (panelLayout === "auto" && host ? _jsx(HostyPanelWorkspace, { children: content }) : content) }), !allowed && _jsxs("div", { "data-hosty-sdk": true, role: "status", className: "hosty-panel-notice", children: [_jsx(SdkChromeStyles, {}), environment?.connection === "blocked" ? _jsxs(_Fragment, { children: [_jsx("strong", { children: environment.code === "actor_mismatch" ? "Use the same Hosty account in this app and its host" : "Cannot verify this app’s host" }), _jsx("p", { children: "Check your account and access in Hosty Core, then reconnect. Your existing discussion is unchanged." }), _jsx("button", { type: "button", onClick: () => window.location.reload(), children: "Reconnect" })] }) : "Connecting to the workspace…"] }), host && _jsx(HostyPresentationLayer, { layer: layer, host: host, active: !!allowed })] }) }) });
57
+ return _jsx(UiHostContext.Provider, { value: host, children: _jsx(UiCompositionContext.Provider, { value: clientRef.current?.composition ?? null, children: _jsxs(UiFrameLayerContext.Provider, { value: layer, children: [_jsx("style", { children: `.hosty-app-layout{display:flex;width:100%;height:100%;min-width:0;min-height:0}.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto;outline:none}` }), _jsx("div", { hidden: !allowed, inert: !allowed, style: { display: allowed ? "contents" : "none" }, children: mounted.current && (panelLayout === "auto" && host ? _jsx(HostyPanelWorkspace, { children: content }) : content) }), !allowed && _jsxs("div", { "data-hosty-sdk": true, role: "status", className: "hosty-panel-notice", children: [_jsx(SdkChromeStyles, {}), environment?.connection === "blocked" ? _jsxs(_Fragment, { children: [_jsx("strong", { children: environment.code === "actor_mismatch" ? "Use the same Hosty account in this app and its host" : "Cannot verify this app’s host" }), _jsx("p", { children: "Check your account and access in Hosty Core, then reconnect. Your existing discussion is unchanged." }), _jsx("button", { type: "button", onClick: () => window.location.reload(), children: "Reconnect" })] }) : "Connecting to the workspace…"] }), _jsx(FeedbackCapture, { active: active && !!allowed, uiPath: uiPath }), host && _jsx(HostyPresentationLayer, { layer: layer, host: host, active: !!allowed })] }) }) });
57
58
  }
58
59
  const empty = { bootstrap: null, catalog: [], tools: [], selected: null, expanded: false, width: 360, error: null };
59
60
  const noop = () => () => { };
package/dist/ui-server.js CHANGED
@@ -1,3 +1,7 @@
1
+ import { FeedbackServerClient } from "@hosty-sdk/app/feedback/server";
2
+ import { FeedbackError } from "@hosty-sdk/app/feedback";
3
+ import { ProviderClient } from "@hosty-sdk/app/providers/server";
4
+ import { ProviderError } from "@hosty-sdk/app/providers";
1
5
  import { UiError } from "@hosty-sdk/app/ui/types";
2
6
  /** Server transport for generic UI metadata. Credentials never appear in returned metadata. */
3
7
  export class UiServerClient {
@@ -63,7 +67,38 @@ export async function createHostyUiResponse(request, context, options = {}) {
63
67
  const route = url.pathname.startsWith(prefix + "/") ? url.pathname.slice(prefix.length + 1) : "";
64
68
  const client = new UiServerClient(options);
65
69
  let result;
66
- if (request.method === "GET" && route === "bootstrap") {
70
+ if (/^feedback\/[a-f0-9]{64}\/review-targets$/.test(route) && request.method === "GET") {
71
+ result = await new FeedbackServerClient(options).reviewTargets(route.split("/")[1], context.token, request.signal);
72
+ }
73
+ else if (route === "feedback/speech" && request.method === "GET") {
74
+ const speech = new ProviderClient(options), permissions = await speech.permissions(request.signal);
75
+ const granted = permissions.granted.includes("providers.speech-to-text");
76
+ result = { granted, providers: granted ? await speech.list("speech-to-text", request.signal) : [] };
77
+ }
78
+ else if (route === "feedback/speech" && request.method === "POST") {
79
+ if (!sameOrigin(request) || request.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json")
80
+ throw new UiError("origin_denied", "Dictate from this application page.", 403);
81
+ const speech = new ProviderClient(options), roster = await speech.list("speech-to-text", request.signal);
82
+ const input = JSON.parse(await boundedBody(request, 6 * 1024 * 1024));
83
+ if (!input || typeof input.appId !== "string" || typeof input.key !== "string" || typeof input.audio !== "string")
84
+ throw new UiError("speech_invalid", "Choose a speech provider and a recording.", 400);
85
+ if (!roster.some(p => p.appId === input.appId && p.key === input.key && p.available))
86
+ throw new UiError("speech_unavailable", "The selected speech provider is unavailable.", 409);
87
+ const bytes = Uint8Array.from(Buffer.from(input.audio, "base64"));
88
+ if (bytes.length < 44 || bytes.length > 120 * 16000 * 2 + 4096 || String.fromCharCode(...bytes.subarray(0, 4)) !== "RIFF" || String.fromCharCode(...bytes.subarray(8, 12)) !== "WAVE")
89
+ throw new UiError("speech_invalid", "Use a bounded WAV recording.", 400);
90
+ result = await speech.transcribe(input, new Blob([bytes], { type: "audio/wav" }), { signal: request.signal });
91
+ }
92
+ else if (route === "feedback" && request.method === "GET") {
93
+ result = await new FeedbackServerClient(options).capabilities(context.token, request.signal);
94
+ }
95
+ else if (route === "feedback" && request.method === "POST") {
96
+ if (!sameOrigin(request) || request.headers.get("content-type")?.split(";", 1)[0]?.trim() !== "application/json")
97
+ throw new UiError("origin_denied", "Submit feedback from this application page.", 403);
98
+ const body = await boundedBody(request, 12 * 1024 * 1024);
99
+ result = await new FeedbackServerClient(options).submit(JSON.parse(body), context.token, request.signal);
100
+ }
101
+ else if (request.method === "GET" && route === "bootstrap") {
67
102
  if ([...url.searchParams.keys()].some(k => k !== "parentOrigin") || url.searchParams.getAll("parentOrigin").length > 1)
68
103
  throw new UiError("ui_request_invalid", "Invalid bootstrap request.", 400);
69
104
  result = await client.bootstrap({ parentOrigin: url.searchParams.get("parentOrigin") ?? undefined }, context.token, request.signal);
@@ -93,12 +128,12 @@ export async function createHostyUiResponse(request, context, options = {}) {
93
128
  return Response.json(result, { headers: responseHeaders });
94
129
  }
95
130
  catch (error) {
96
- const known = error instanceof UiError;
131
+ const known = error instanceof UiError || error instanceof FeedbackError || error instanceof ProviderError;
97
132
  return Response.json({ code: known ? error.code : error instanceof SyntaxError ? "ui_request_invalid" : "ui_unavailable",
98
133
  message: known ? error.message : "Application panels are unavailable. Retry opening the same target." }, { status: known ? error.status : error instanceof SyntaxError ? 400 : 503, headers: responseHeaders });
99
134
  }
100
135
  }
101
- async function boundedBody(request) {
136
+ async function boundedBody(request, limit = 16_384) {
102
137
  const reader = request.body?.getReader();
103
138
  if (!reader)
104
139
  return "";
@@ -110,7 +145,7 @@ async function boundedBody(request) {
110
145
  if (done)
111
146
  break;
112
147
  length += value.byteLength;
113
- if (length > 16_384) {
148
+ if (length > limit) {
114
149
  await reader.cancel();
115
150
  throw new UiError("ui_request_invalid", "Panel request is too large.", 413);
116
151
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.37.0",
3
+ "version": "0.39.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",
@@ -140,6 +140,22 @@
140
140
  "./tasks/server": {
141
141
  "types": "./dist/tasks-server.d.ts",
142
142
  "default": "./dist/tasks-server.js"
143
+ },
144
+ "./feedback": {
145
+ "types": "./dist/feedback-types.d.ts",
146
+ "default": "./dist/feedback-types.js"
147
+ },
148
+ "./feedback/server": {
149
+ "types": "./dist/feedback-server.d.ts",
150
+ "default": "./dist/feedback-server.js"
151
+ },
152
+ "./agent-sessions": {
153
+ "types": "./dist/agent-sessions.d.ts",
154
+ "default": "./dist/agent-sessions.js"
155
+ },
156
+ "./history": {
157
+ "types": "./dist/history.d.ts",
158
+ "default": "./dist/history.js"
143
159
  }
144
160
  },
145
161
  "scripts": {