@hosty-sdk/app 0.35.1 → 0.38.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 +63 -6
- package/dist/feedback-audio.d.ts +3 -0
- package/dist/feedback-audio.js +40 -0
- package/dist/feedback-capture.d.ts +3 -0
- package/dist/feedback-capture.js +80 -0
- package/dist/feedback-draft.d.ts +20 -0
- package/dist/feedback-draft.js +104 -0
- package/dist/feedback-react.d.ts +4 -0
- package/dist/feedback-react.js +390 -0
- package/dist/feedback-server.d.ts +31 -0
- package/dist/feedback-server.js +48 -0
- package/dist/feedback-speech.d.ts +6 -0
- package/dist/feedback-speech.js +119 -0
- package/dist/feedback-types.d.ts +122 -0
- package/dist/feedback-types.js +9 -0
- package/dist/index.d.ts +0 -5
- package/dist/index.js +0 -21
- package/dist/overlay-react.js +3 -2
- package/dist/permissions-react.js +1 -3
- package/dist/permissions.js +3 -3
- package/dist/providers-server.d.ts +2 -0
- package/dist/providers-server.js +13 -0
- package/dist/providers.d.ts +2 -2
- package/dist/sandbox-server.d.ts +1 -115
- package/dist/sandbox-server.js +0 -85
- package/dist/server.d.ts +0 -3
- package/dist/server.js +3 -15
- package/dist/session-server.d.ts +0 -2
- package/dist/session-server.js +4 -9
- package/dist/tasks-server.d.ts +33 -0
- package/dist/tasks-server.js +57 -0
- package/dist/tasks.d.ts +148 -0
- package/dist/tasks.js +2 -0
- package/dist/ui-frame.js +1 -1
- package/dist/ui-react.js +2 -1
- package/dist/ui-server.d.ts +0 -2
- package/dist/ui-server.js +39 -6
- package/package.json +17 -1
- package/dist/sandbox-identity.d.ts +0 -10
- package/dist/sandbox-identity.js +0 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { ProviderClient } from "./providers-server.js";
|
|
2
|
+
import type { TaskProviderRef, TaskRef, TaskRecord, TaskPage, TaskCreate, TaskEdit, TaskAction, TaskStatus, TaskCapabilities, TaskExecutorOptions, TaskExecutionInput, TaskExecutionReceipt } from "./tasks.js";
|
|
3
|
+
/** Server-side task API. Every request obtains a fresh Core binding; no mutation is retried implicitly. */
|
|
4
|
+
export declare class TaskClient {
|
|
5
|
+
private readonly providers;
|
|
6
|
+
private readonly provider;
|
|
7
|
+
private readonly userToken?;
|
|
8
|
+
constructor(providers: ProviderClient, provider: TaskProviderRef, userToken?: (() => Promise<string>) | undefined);
|
|
9
|
+
private call;
|
|
10
|
+
private id;
|
|
11
|
+
capabilities(signal?: AbortSignal): Promise<TaskCapabilities>;
|
|
12
|
+
list(options?: {
|
|
13
|
+
cursor?: string;
|
|
14
|
+
limit?: number;
|
|
15
|
+
status?: TaskStatus;
|
|
16
|
+
signal?: AbortSignal;
|
|
17
|
+
}): Promise<TaskPage>;
|
|
18
|
+
create(input: TaskCreate, signal?: AbortSignal): Promise<TaskRecord>;
|
|
19
|
+
get(ref: TaskRef, signal?: AbortSignal): Promise<TaskRecord>;
|
|
20
|
+
edit(ref: TaskRef, input: TaskEdit, signal?: AbortSignal): Promise<TaskRecord>;
|
|
21
|
+
action(ref: TaskRef, input: TaskAction, signal?: AbortSignal): Promise<TaskRecord>;
|
|
22
|
+
}
|
|
23
|
+
export declare class TaskExecutorClient {
|
|
24
|
+
private readonly providers;
|
|
25
|
+
private readonly executor;
|
|
26
|
+
private readonly userToken?;
|
|
27
|
+
constructor(providers: ProviderClient, executor: TaskProviderRef, userToken?: (() => Promise<string>) | undefined);
|
|
28
|
+
private call;
|
|
29
|
+
options(signal?: AbortSignal): Promise<TaskExecutorOptions>;
|
|
30
|
+
accept(executionId: string, input: TaskExecutionInput, signal?: AbortSignal): Promise<TaskExecutionReceipt>;
|
|
31
|
+
get(executionId: string, signal?: AbortSignal): Promise<TaskExecutionReceipt>;
|
|
32
|
+
cancel(executionId: string, requestId: string, signal?: AbortSignal): Promise<TaskExecutionReceipt>;
|
|
33
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/** Server-side task API. Every request obtains a fresh Core binding; no mutation is retried implicitly. */
|
|
2
|
+
export class TaskClient {
|
|
3
|
+
providers;
|
|
4
|
+
provider;
|
|
5
|
+
userToken;
|
|
6
|
+
constructor(providers, provider, userToken) {
|
|
7
|
+
this.providers = providers;
|
|
8
|
+
this.provider = provider;
|
|
9
|
+
this.userToken = userToken;
|
|
10
|
+
}
|
|
11
|
+
call(route, method = "GET", input, signal) {
|
|
12
|
+
return this.providers.taskRequest(this.provider, "tasks", route, method, input, this.userToken, signal);
|
|
13
|
+
}
|
|
14
|
+
id(ref) {
|
|
15
|
+
if (ref.appId !== this.provider.appId || ref.installationId !== this.provider.installationId || ref.key !== this.provider.key)
|
|
16
|
+
throw new Error("The task belongs to another provider installation.");
|
|
17
|
+
return encodeURIComponent(ref.id);
|
|
18
|
+
}
|
|
19
|
+
capabilities(signal) { return this.call("/capabilities", "GET", undefined, signal); }
|
|
20
|
+
list(options = {}) {
|
|
21
|
+
const query = new URLSearchParams();
|
|
22
|
+
if (options.cursor)
|
|
23
|
+
query.set("cursor", options.cursor);
|
|
24
|
+
if (options.limit !== undefined)
|
|
25
|
+
query.set("limit", String(options.limit));
|
|
26
|
+
if (options.status)
|
|
27
|
+
query.set("status", options.status);
|
|
28
|
+
return this.call(`/tasks?${query}`, "GET", undefined, options.signal);
|
|
29
|
+
}
|
|
30
|
+
create(input, signal) { return this.call("/tasks", "POST", input, signal); }
|
|
31
|
+
get(ref, signal) { return this.call(`/tasks/${this.id(ref)}`, "GET", undefined, signal); }
|
|
32
|
+
edit(ref, input, signal) { return this.call(`/tasks/${this.id(ref)}`, "PATCH", input, signal); }
|
|
33
|
+
action(ref, input, signal) { return this.call(`/tasks/${this.id(ref)}/actions`, "POST", input, signal); }
|
|
34
|
+
}
|
|
35
|
+
export class TaskExecutorClient {
|
|
36
|
+
providers;
|
|
37
|
+
executor;
|
|
38
|
+
userToken;
|
|
39
|
+
constructor(providers, executor, userToken) {
|
|
40
|
+
this.providers = providers;
|
|
41
|
+
this.executor = executor;
|
|
42
|
+
this.userToken = userToken;
|
|
43
|
+
}
|
|
44
|
+
call(route, method = "GET", input, signal) {
|
|
45
|
+
return this.providers.taskRequest(this.executor, "task-executor", route, method, input, this.userToken, signal);
|
|
46
|
+
}
|
|
47
|
+
options(signal) { return this.call("/options", "GET", undefined, signal); }
|
|
48
|
+
accept(executionId, input, signal) {
|
|
49
|
+
return this.call(`/executions/${encodeURIComponent(executionId)}`, "PUT", input, signal);
|
|
50
|
+
}
|
|
51
|
+
get(executionId, signal) {
|
|
52
|
+
return this.call(`/executions/${encodeURIComponent(executionId)}`, "GET", undefined, signal);
|
|
53
|
+
}
|
|
54
|
+
cancel(executionId, requestId, signal) {
|
|
55
|
+
return this.call(`/executions/${encodeURIComponent(executionId)}/cancel`, "POST", { requestId }, signal);
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/tasks.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/** Core's version-one task contract. Numbers are opaque; references bind an installation. */
|
|
2
|
+
export type TaskProviderRef = {
|
|
3
|
+
appId: string;
|
|
4
|
+
installationId: string;
|
|
5
|
+
key: string;
|
|
6
|
+
};
|
|
7
|
+
export type TaskRef = TaskProviderRef & {
|
|
8
|
+
id: string;
|
|
9
|
+
};
|
|
10
|
+
export type TaskStatus = "Draft" | "Ready" | "InProgress" | "Blocked" | "Done" | "Cancelled";
|
|
11
|
+
export type TaskResource = {
|
|
12
|
+
kind: "app" | "workspace" | "external";
|
|
13
|
+
owner: string;
|
|
14
|
+
id: string;
|
|
15
|
+
label?: string;
|
|
16
|
+
version: {
|
|
17
|
+
mode: "live";
|
|
18
|
+
} | {
|
|
19
|
+
mode: "pinned";
|
|
20
|
+
revision: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export type TaskAssignee = {
|
|
24
|
+
kind: "human";
|
|
25
|
+
userId: string;
|
|
26
|
+
name?: string;
|
|
27
|
+
} | {
|
|
28
|
+
kind: "app";
|
|
29
|
+
executor: TaskProviderRef;
|
|
30
|
+
optionId?: string;
|
|
31
|
+
name?: string;
|
|
32
|
+
};
|
|
33
|
+
export type TaskInput = {
|
|
34
|
+
title: string;
|
|
35
|
+
markdown: string;
|
|
36
|
+
assignee: TaskAssignee | null;
|
|
37
|
+
requiresReview: boolean;
|
|
38
|
+
resources: TaskResource[];
|
|
39
|
+
workspaceId?: string;
|
|
40
|
+
};
|
|
41
|
+
export type TaskActor = {
|
|
42
|
+
userId: string | null;
|
|
43
|
+
appId: string | null;
|
|
44
|
+
installationId?: string;
|
|
45
|
+
};
|
|
46
|
+
export type TaskResult = {
|
|
47
|
+
markdown: string;
|
|
48
|
+
resources: TaskResource[];
|
|
49
|
+
};
|
|
50
|
+
export type TaskExecutionState = "accepted" | "running" | "awaiting_input" | "cancelling" | "completed" | "failed" | "cancelled" | "interrupted";
|
|
51
|
+
export type TaskExecutionInput = {
|
|
52
|
+
taskRef: TaskRef;
|
|
53
|
+
taskRevision: number;
|
|
54
|
+
executorOptionId?: string;
|
|
55
|
+
input: Omit<TaskInput, "assignee">;
|
|
56
|
+
};
|
|
57
|
+
export type TaskExecutionReceipt = {
|
|
58
|
+
executionId: string;
|
|
59
|
+
revision: number;
|
|
60
|
+
state: TaskExecutionState;
|
|
61
|
+
stopConfirmed: boolean;
|
|
62
|
+
createdAt: string;
|
|
63
|
+
updatedAt: string;
|
|
64
|
+
result?: TaskResult;
|
|
65
|
+
error?: {
|
|
66
|
+
code: string;
|
|
67
|
+
message: string;
|
|
68
|
+
}; /** An absolute HTTP(S) UI URL for cross-app readers; same-app readers can resolve a local path. */
|
|
69
|
+
sessionRef?: {
|
|
70
|
+
id: string;
|
|
71
|
+
path: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
export type TaskAttempt = {
|
|
75
|
+
executionId: string;
|
|
76
|
+
executor: TaskProviderRef;
|
|
77
|
+
request: TaskExecutionInput;
|
|
78
|
+
receipt?: TaskExecutionReceipt;
|
|
79
|
+
cancelRequested: boolean;
|
|
80
|
+
createdAt: string;
|
|
81
|
+
};
|
|
82
|
+
export type TaskOutcome = {
|
|
83
|
+
inputRevision: number;
|
|
84
|
+
result: TaskResult;
|
|
85
|
+
recordedAt: string;
|
|
86
|
+
executionId?: string;
|
|
87
|
+
};
|
|
88
|
+
export type TaskRecord = TaskInput & {
|
|
89
|
+
ref: TaskRef;
|
|
90
|
+
number: string;
|
|
91
|
+
revision: number;
|
|
92
|
+
inputRevision: number;
|
|
93
|
+
status: TaskStatus;
|
|
94
|
+
createdBy: TaskActor;
|
|
95
|
+
owner: TaskActor;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
blocked?: {
|
|
99
|
+
reason: string;
|
|
100
|
+
message: string;
|
|
101
|
+
};
|
|
102
|
+
cancelRequested: boolean;
|
|
103
|
+
result?: TaskResult;
|
|
104
|
+
resultInputRevision?: number;
|
|
105
|
+
attempts: TaskAttempt[];
|
|
106
|
+
outcomes?: TaskOutcome[];
|
|
107
|
+
};
|
|
108
|
+
export type TaskMutation = {
|
|
109
|
+
requestId: string;
|
|
110
|
+
expectedRevision: number;
|
|
111
|
+
};
|
|
112
|
+
export type TaskInputWrite = Omit<TaskInput, "requiresReview"> & {
|
|
113
|
+
requiresReview?: boolean;
|
|
114
|
+
};
|
|
115
|
+
export type TaskCreate = {
|
|
116
|
+
requestId: string;
|
|
117
|
+
input: TaskInputWrite;
|
|
118
|
+
};
|
|
119
|
+
export type TaskEdit = TaskMutation & {
|
|
120
|
+
input: TaskInputWrite;
|
|
121
|
+
};
|
|
122
|
+
export type TaskAction = TaskMutation & {
|
|
123
|
+
action: "ready" | "draft" | "start" | "complete" | "cancel" | "accept" | "request_changes";
|
|
124
|
+
result?: TaskResult;
|
|
125
|
+
resultInputRevision?: number;
|
|
126
|
+
feedback?: string;
|
|
127
|
+
};
|
|
128
|
+
export type TaskPage = {
|
|
129
|
+
tasks: TaskRecord[];
|
|
130
|
+
nextCursor: string | null;
|
|
131
|
+
};
|
|
132
|
+
export type TaskCapabilities = {
|
|
133
|
+
version: 1;
|
|
134
|
+
prefix: string;
|
|
135
|
+
statuses: TaskStatus[];
|
|
136
|
+
maxPageSize: number;
|
|
137
|
+
};
|
|
138
|
+
export type TaskExecutorOption = {
|
|
139
|
+
id: string;
|
|
140
|
+
name: string;
|
|
141
|
+
capabilities: string[];
|
|
142
|
+
};
|
|
143
|
+
export type TaskExecutorOptions = {
|
|
144
|
+
options: TaskExecutorOption[];
|
|
145
|
+
defaultId: string | null;
|
|
146
|
+
};
|
|
147
|
+
export declare const taskStatuses: TaskStatus[];
|
|
148
|
+
export declare const taskExecutionTerminal: (state: TaskExecutionState) => boolean;
|
package/dist/tasks.js
ADDED
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.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { SandboxIdentityContext } from "./sandbox-identity.js";
|
|
2
1
|
import type { ProviderServerOptions } from "@hosty-sdk/app/providers/server";
|
|
3
2
|
import { type ToolActivity, type PanelTarget, type UiBootstrap, type UiPanel, type UiResolvedPanel, type UiNavigationCatalog, type UiNavigationTarget, type UiResolvedNavigation } from "@hosty-sdk/app/ui/types";
|
|
4
3
|
/** Server transport for generic UI metadata. Credentials never appear in returned metadata. */
|
|
@@ -22,7 +21,6 @@ type UiAuthenticatedContext = {
|
|
|
22
21
|
status: "active";
|
|
23
22
|
identity: {
|
|
24
23
|
hostRole: string | null;
|
|
25
|
-
sandbox?: SandboxIdentityContext;
|
|
26
24
|
};
|
|
27
25
|
} | {
|
|
28
26
|
status: "not-present" | "expired" | "forbidden" | "unavailable" | "misconfigured";
|
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 {
|
|
@@ -58,14 +62,43 @@ export async function createHostyUiResponse(request, context, options = {}) {
|
|
|
58
62
|
}
|
|
59
63
|
if (options.administratorOnly && context.session.identity.hostRole !== "host.admin")
|
|
60
64
|
throw new UiError("admin_required", "This application requires administrator access.", 403);
|
|
61
|
-
if (context.session.identity.sandbox)
|
|
62
|
-
throw new UiError("unsupported_sandbox_ui", "Global application panels are unavailable inside this sandbox profile.", 409);
|
|
63
65
|
const url = new URL(request.url);
|
|
64
66
|
const prefix = (options.uiPath ?? "/api/hosty/ui").replace(/\/$/, "");
|
|
65
67
|
const route = url.pathname.startsWith(prefix + "/") ? url.pathname.slice(prefix.length + 1) : "";
|
|
66
68
|
const client = new UiServerClient(options);
|
|
67
69
|
let result;
|
|
68
|
-
if (request.method === "GET"
|
|
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") {
|
|
69
102
|
if ([...url.searchParams.keys()].some(k => k !== "parentOrigin") || url.searchParams.getAll("parentOrigin").length > 1)
|
|
70
103
|
throw new UiError("ui_request_invalid", "Invalid bootstrap request.", 400);
|
|
71
104
|
result = await client.bootstrap({ parentOrigin: url.searchParams.get("parentOrigin") ?? undefined }, context.token, request.signal);
|
|
@@ -95,12 +128,12 @@ export async function createHostyUiResponse(request, context, options = {}) {
|
|
|
95
128
|
return Response.json(result, { headers: responseHeaders });
|
|
96
129
|
}
|
|
97
130
|
catch (error) {
|
|
98
|
-
const known = error instanceof UiError;
|
|
131
|
+
const known = error instanceof UiError || error instanceof FeedbackError || error instanceof ProviderError;
|
|
99
132
|
return Response.json({ code: known ? error.code : error instanceof SyntaxError ? "ui_request_invalid" : "ui_unavailable",
|
|
100
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 });
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
|
-
async function boundedBody(request) {
|
|
136
|
+
async function boundedBody(request, limit = 16_384) {
|
|
104
137
|
const reader = request.body?.getReader();
|
|
105
138
|
if (!reader)
|
|
106
139
|
return "";
|
|
@@ -112,7 +145,7 @@ async function boundedBody(request) {
|
|
|
112
145
|
if (done)
|
|
113
146
|
break;
|
|
114
147
|
length += value.byteLength;
|
|
115
|
-
if (length >
|
|
148
|
+
if (length > limit) {
|
|
116
149
|
await reader.cancel();
|
|
117
150
|
throw new UiError("ui_request_invalid", "Panel request is too large.", 413);
|
|
118
151
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hosty-sdk/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.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",
|
|
@@ -132,6 +132,22 @@
|
|
|
132
132
|
"./tool-activity/server": {
|
|
133
133
|
"types": "./dist/tool-activity-server.d.ts",
|
|
134
134
|
"default": "./dist/tool-activity-server.js"
|
|
135
|
+
},
|
|
136
|
+
"./tasks": {
|
|
137
|
+
"types": "./dist/tasks.d.ts",
|
|
138
|
+
"default": "./dist/tasks.js"
|
|
139
|
+
},
|
|
140
|
+
"./tasks/server": {
|
|
141
|
+
"types": "./dist/tasks-server.d.ts",
|
|
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"
|
|
135
151
|
}
|
|
136
152
|
},
|
|
137
153
|
"scripts": {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
export interface SandboxTarget {
|
|
2
|
-
runtimeInstanceId: string;
|
|
3
|
-
generation: string;
|
|
4
|
-
dataRevision: string;
|
|
5
|
-
}
|
|
6
|
-
export interface SandboxIdentityContext extends SandboxTarget {
|
|
7
|
-
identityRealm: "sandbox" | "core";
|
|
8
|
-
appRole: string;
|
|
9
|
-
permissions: string[];
|
|
10
|
-
}
|
package/dist/sandbox-identity.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|