@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,122 @@
|
|
|
1
|
+
export type FeedbackRect = {
|
|
2
|
+
x: number;
|
|
3
|
+
y: number;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
};
|
|
7
|
+
export type FeedbackTarget = {
|
|
8
|
+
kind: "element" | "region" | "page";
|
|
9
|
+
label?: string;
|
|
10
|
+
selector?: string;
|
|
11
|
+
bounds?: FeedbackRect;
|
|
12
|
+
viewport?: {
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
scale: number;
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
export type FeedbackNoteInput = {
|
|
19
|
+
id: string;
|
|
20
|
+
text: string;
|
|
21
|
+
route: string;
|
|
22
|
+
capturedAt: string;
|
|
23
|
+
target?: FeedbackTarget;
|
|
24
|
+
image?: {
|
|
25
|
+
mediaType: string;
|
|
26
|
+
data: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
export type FeedbackSubmit = {
|
|
30
|
+
requestId: string;
|
|
31
|
+
comment: string;
|
|
32
|
+
annotations: FeedbackNoteInput[];
|
|
33
|
+
scope?: {
|
|
34
|
+
coreId: string;
|
|
35
|
+
appId: string;
|
|
36
|
+
installation: string;
|
|
37
|
+
userId: string;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
export type FeedbackCapabilities = {
|
|
41
|
+
version: 1;
|
|
42
|
+
coreId: string;
|
|
43
|
+
appId: string;
|
|
44
|
+
installation: string;
|
|
45
|
+
userId: string;
|
|
46
|
+
maxAnnotations: number;
|
|
47
|
+
maxImageBytes: number;
|
|
48
|
+
maxTotalImageBytes: number;
|
|
49
|
+
administrator?: boolean;
|
|
50
|
+
};
|
|
51
|
+
export type FeedbackNote = Omit<FeedbackNoteInput, "image"> & {
|
|
52
|
+
mediaType: string | null;
|
|
53
|
+
};
|
|
54
|
+
export type FeedbackReport = {
|
|
55
|
+
id: string;
|
|
56
|
+
reporterId: string;
|
|
57
|
+
comment: string;
|
|
58
|
+
annotations: FeedbackNote[];
|
|
59
|
+
context: {
|
|
60
|
+
appId: string;
|
|
61
|
+
appName: string;
|
|
62
|
+
installation: string;
|
|
63
|
+
version: string | null;
|
|
64
|
+
observedAt: string;
|
|
65
|
+
executionKind: string;
|
|
66
|
+
workspaceId: string | null;
|
|
67
|
+
sourceRevision: string | null;
|
|
68
|
+
};
|
|
69
|
+
workspaceId: string | null;
|
|
70
|
+
revision: number;
|
|
71
|
+
status: string;
|
|
72
|
+
currentAttemptId: string | null;
|
|
73
|
+
disposition: string | null;
|
|
74
|
+
};
|
|
75
|
+
export type FeedbackSelection = {
|
|
76
|
+
id: string;
|
|
77
|
+
revision: number;
|
|
78
|
+
};
|
|
79
|
+
export type FeedbackDispatchInput = {
|
|
80
|
+
requestId: string;
|
|
81
|
+
sessionId: string;
|
|
82
|
+
workspaceId: string | null;
|
|
83
|
+
reports: FeedbackSelection[];
|
|
84
|
+
instruction: string;
|
|
85
|
+
appContextRevision?: number;
|
|
86
|
+
selectionRevision?: number;
|
|
87
|
+
};
|
|
88
|
+
export type FeedbackOutcome = {
|
|
89
|
+
reportId: string;
|
|
90
|
+
summary: string;
|
|
91
|
+
handledAnnotationIds: string[];
|
|
92
|
+
handled: boolean;
|
|
93
|
+
};
|
|
94
|
+
export type FeedbackDispatch = {
|
|
95
|
+
id: string;
|
|
96
|
+
callerAppId: string;
|
|
97
|
+
callerInstallation: string;
|
|
98
|
+
actorId: string;
|
|
99
|
+
input: FeedbackDispatchInput;
|
|
100
|
+
snapshot: FeedbackReport[];
|
|
101
|
+
createdAt: string;
|
|
102
|
+
state: string;
|
|
103
|
+
messageSequence: number | null;
|
|
104
|
+
workspaceId: string | null;
|
|
105
|
+
outcomes: FeedbackOutcome[] | null;
|
|
106
|
+
error: string | null;
|
|
107
|
+
};
|
|
108
|
+
export type FeedbackCollection = {
|
|
109
|
+
reports: FeedbackReport[];
|
|
110
|
+
dispatches: FeedbackDispatch[];
|
|
111
|
+
};
|
|
112
|
+
export type FeedbackDelivery = {
|
|
113
|
+
state: string;
|
|
114
|
+
messageSequence?: number;
|
|
115
|
+
workspaceId?: string | null;
|
|
116
|
+
error?: string;
|
|
117
|
+
};
|
|
118
|
+
export declare class FeedbackError extends Error {
|
|
119
|
+
readonly code: string;
|
|
120
|
+
readonly status: number;
|
|
121
|
+
constructor(code: string, message: string, status: number);
|
|
122
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -206,8 +206,3 @@ export declare function decideRecoveryAction(input: {
|
|
|
206
206
|
}): RecoveryAction;
|
|
207
207
|
/** Paint-time navigation policy. Kept in the pure entry point for Node and edge consumers. */
|
|
208
208
|
export declare function chromeBootstrapScript(defaults?: import("./ui-types").UiChromeOverrides): string;
|
|
209
|
-
/** Execution scope is independent of the principal's role. Never infer it from an app ID. */
|
|
210
|
-
import type { SandboxTarget, SandboxIdentityContext } from "./sandbox-identity.js";
|
|
211
|
-
export type { SandboxTarget, SandboxIdentityContext } from "./sandbox-identity.js";
|
|
212
|
-
export declare function readSandboxTarget(environment: Record<string, string | undefined>): SandboxTarget | null;
|
|
213
|
-
export declare function readSandboxIdentity(payload: Record<string, unknown>, expected: SandboxTarget | null): SandboxIdentityContext | null;
|
package/dist/index.js
CHANGED
|
@@ -269,24 +269,3 @@ export function chromeBootstrapScript(defaults = {}) {
|
|
|
269
269
|
throw new Error("Invalid navigation default.");
|
|
270
270
|
return `(()=>{try{const d=${JSON.stringify({ navigation })},p=new URLSearchParams(location.search),h=new URLSearchParams();for(const k of ["hosty_navigation","hosty_tools","hosty_tool_launcher","hosty_launch"])for(const v of p.getAll(k))h.append(k,v);document.documentElement.setAttribute('data-hosty-chrome-hints',h.toString());const one=k=>p.getAll(k).length===1?p.get(k):null,n=one('hosty_navigation'),l=one('hosty_launch');document.documentElement.setAttribute('data-hosty-navigation',n==='show'||n==='hide'?n:d.navigation??(l==='native'||l==='embedded'?'hide':'show'));}catch{}})();`;
|
|
271
271
|
}
|
|
272
|
-
export function readSandboxTarget(environment) {
|
|
273
|
-
const fields = [environment.HOSTY_SANDBOX_INSTANCE_ID, environment.HOSTY_SANDBOX_GENERATION, environment.HOSTY_SANDBOX_DATA_REVISION];
|
|
274
|
-
if (!environment.HOSTY_SANDBOX_PROTOCOL && fields.every(value => !value))
|
|
275
|
-
return null;
|
|
276
|
-
if (environment.HOSTY_SANDBOX_PROTOCOL !== "hosty-sandbox-v1" || fields.some(value => !/^[a-f0-9]{32}$/.test(value ?? "")))
|
|
277
|
-
throw new Error("The sandbox execution target is missing or unsupported.");
|
|
278
|
-
return { runtimeInstanceId: fields[0], generation: fields[1], dataRevision: fields[2] };
|
|
279
|
-
}
|
|
280
|
-
export function readSandboxIdentity(payload, expected) {
|
|
281
|
-
if (!expected) {
|
|
282
|
-
if (payload.identityRealm === "sandbox" || payload.runtimeInstanceId != null || payload.generation != null || payload.dataRevision != null)
|
|
283
|
-
throw new Error("An installed app cannot accept a sandbox identity.");
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
if (payload.runtimeInstanceId !== expected.runtimeInstanceId || payload.generation !== expected.generation || payload.dataRevision !== expected.dataRevision
|
|
287
|
-
|| (payload.identityRealm !== "sandbox" && payload.identityRealm !== "core") || typeof payload.appRole !== "string"
|
|
288
|
-
|| !Array.isArray(payload.permissions) || !payload.permissions.every(value => typeof value === "string")
|
|
289
|
-
|| (payload.identityRealm === "sandbox" && payload.hostRole != null))
|
|
290
|
-
throw new Error("The identity belongs to a different or unsupported sandbox target.");
|
|
291
|
-
return { ...expected, identityRealm: payload.identityRealm, appRole: payload.appRole, permissions: payload.permissions };
|
|
292
|
-
}
|
package/dist/overlay-react.js
CHANGED
|
@@ -7,7 +7,7 @@ import { useAppActivity } from "./activity-react.js";
|
|
|
7
7
|
import { APP_SESSION_ENDED, APP_SESSION_RESTORED, APP_ACTIVITY_RENEWED, acceptAppAuthProtocol } from "./browser-auth.js";
|
|
8
8
|
import { buildCoreOpenUrl, readRecoveryParams } from "./index.js";
|
|
9
9
|
import { permissionReviewUrl, requestPermissionReview } from "./permissions.js";
|
|
10
|
-
import { SdkBrand, SdkChromeStyles, SdkLoader } from "./sdk-chrome.js";
|
|
10
|
+
import { SdkBrand, SdkChromeStyles, SdkIcon, SdkLoader } from "./sdk-chrome.js";
|
|
11
11
|
import { HostyUiRoot } from "./ui-react.js";
|
|
12
12
|
function snapshot(body) {
|
|
13
13
|
const value = body;
|
|
@@ -19,6 +19,7 @@ function snapshot(body) {
|
|
|
19
19
|
const openUrl = recovery.appId ? buildCoreOpenUrl(recovery.corePublicOrigin, recovery.appId, window.location) : null;
|
|
20
20
|
// The activity policy remains authoritative even when renewal discovery fails.
|
|
21
21
|
return { userId: value.userId, setup, notice: value.hosty?.notice,
|
|
22
|
+
permissionWarning: value.hosty?.permissionWarning === "unsupported",
|
|
22
23
|
activityRequired: value.activityRequired === true,
|
|
23
24
|
activeUntil: typeof value.activeUntil === "string" ? value.activeUntil : null,
|
|
24
25
|
recoveryReady: !!openUrl && !!acceptAppAuthProtocol(new URL(openUrl).origin, recovery.appAuthProtocol) };
|
|
@@ -189,7 +190,7 @@ export function HostyOverlay({ children, probePath = "/api/hosty/session", appCo
|
|
|
189
190
|
}
|
|
190
191
|
const localOffline = (identity.ui.kind === "unavailable" || (identity.ui.kind === "active" && session?.setup === "unavailable"))
|
|
191
192
|
&& localContent !== undefined;
|
|
192
|
-
return _jsxs(_Fragment, { children: [localOffline && _jsxs("div", { "data-hosty-local-content": true, children: [_jsxs("div", { "data-hosty-sdk": true, className: "hosty-overlay-offline", role: "status", children: [_jsx(SdkChromeStyles, {}), _jsx("style", { children: styles }), _jsx(SdkBrand, {}), _jsx("span", { children: "Hosty is unavailable. Local content remains available." }), _jsx("button", { type: "button", className: "hosty-sdk-button", onClick: () => { void refreshRef.current(); }, children: "Retry" })] }), localContent] }), _jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { chrome: chrome, navigation: navigation, 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 })] });
|
|
193
|
+
return _jsxs(_Fragment, { children: [ready && session?.permissionWarning && _jsxs("div", { "data-hosty-sdk": true, role: "status", className: "hosty-overlay-offline", children: [_jsx(SdkChromeStyles, {}), _jsx("style", { children: styles }), _jsx(SdkIcon, { name: "warning" }), _jsxs("span", { children: ["This Core does not support some permissions requested by this app. Update the app or Core. These permissions are not granted; some app features may not work.", session.notice?.permissions?.unsupportedRequired?.length ? ` (${session.notice.permissions.unsupportedRequired.join(", ")})` : ""] })] }), localOffline && _jsxs("div", { "data-hosty-local-content": true, children: [_jsxs("div", { "data-hosty-sdk": true, className: "hosty-overlay-offline", role: "status", children: [_jsx(SdkChromeStyles, {}), _jsx("style", { children: styles }), _jsx(SdkBrand, {}), _jsx("span", { children: "Hosty is unavailable. Local content remains available." }), _jsx("button", { type: "button", className: "hosty-sdk-button", onClick: () => { void refreshRef.current(); }, children: "Retry" })] }), localContent] }), _jsx("div", { "data-hosty-content": true, hidden: !ready, inert: !ready, style: ready ? { display: "contents" } : { display: "none" }, children: _jsx(HostyUiRoot, { chrome: chrome, navigation: navigation, 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 })] });
|
|
193
194
|
}
|
|
194
195
|
function OverlaySurface({ pending, title, message, action, actionLabel, reviewUrl }) {
|
|
195
196
|
const [portal, setPortal] = useState(null);
|
|
@@ -42,9 +42,7 @@ export function MissingPermissionsNotice({ endpoint = "/api/hosty/permissions" }
|
|
|
42
42
|
const kind = state && permissionNotice(state);
|
|
43
43
|
if (dismissed || !kind || !state)
|
|
44
44
|
return null;
|
|
45
|
-
return _jsxs("section", { "data-hosty-sdk": true, "data-hosty-activity-control": true, role: "alert", "aria-label": "Required permissions", className: "hosty-permissions-notice", children: [_jsx(SdkChromeStyles, {}), _jsx("style", { children: styles }), _jsxs("header", { children: [_jsx(SdkBrand, {}), _jsx(SdkIcon, { name: "warning" }), _jsx("strong", { children: "Permissions needed" })] }), kind === "
|
|
46
|
-
? _jsxs("p", { children: ["Core does not support required permissions: ", state.permissions.unsupportedRequired.join(", "), ". Update the app or Core."] })
|
|
47
|
-
: _jsx("p", { children: "This app needs required permissions. Operations using them are unavailable until approval." }), kind === "missing" && state.permissions?.reviewAvailable !== false && state.corePublicOrigin &&
|
|
45
|
+
return _jsxs("section", { "data-hosty-sdk": true, "data-hosty-activity-control": true, role: "alert", "aria-label": "Required permissions", className: "hosty-permissions-notice", children: [_jsx(SdkChromeStyles, {}), _jsx("style", { children: styles }), _jsxs("header", { children: [_jsx(SdkBrand, {}), _jsx(SdkIcon, { name: "warning" }), _jsx("strong", { children: "Permissions needed" })] }), !!state.permissions?.unsupportedRequired?.length && _jsxs("p", { children: ["This Core does not support requested permissions: ", state.permissions.unsupportedRequired.join(", "), ". Update the app or Core. These permissions are not granted; some app features may not work."] }), kind === "missing" && _jsx("p", { children: "This app needs required permissions. Operations using them are unavailable until approval." }), kind === "missing" && state.permissions?.reviewAvailable !== false && state.corePublicOrigin &&
|
|
48
46
|
_jsx("button", { type: "button", className: "hosty-sdk-button", onClick: () => { requestPermissionReview(state); setPending(true); }, children: "Request permissions" }), pending && _jsxs("p", { className: "hosty-sdk-status", children: [_jsx(SdkSpinner, {}), "Review permissions in Core, then return here. You can leave this app installed without approving."] }), _jsx("button", { type: "button", onClick: () => { setDismissed(true); setPending(false); }, className: "hosty-sdk-button hosty-sdk-ghost", children: "Dismiss" })] });
|
|
49
47
|
}
|
|
50
48
|
const styles = `
|
package/dist/permissions.js
CHANGED
|
@@ -4,9 +4,9 @@ export function permissionNotice(state) {
|
|
|
4
4
|
if (state.hostRole !== "host.admin" || !state.permissions)
|
|
5
5
|
return null;
|
|
6
6
|
const p = state.permissions;
|
|
7
|
-
if (p.unsupportedRequired?.
|
|
8
|
-
return "
|
|
9
|
-
return p.
|
|
7
|
+
if (p.required.some(name => !p.unsupportedRequired?.includes(name) && !p.granted.includes(name)))
|
|
8
|
+
return "missing";
|
|
9
|
+
return p.unsupportedRequired?.length ? "unsupported" : null;
|
|
10
10
|
}
|
|
11
11
|
export function permissionReviewUrl(corePublicOrigin, appId) {
|
|
12
12
|
const core = new URL(corePublicOrigin);
|
|
@@ -30,6 +30,8 @@ export declare class ProviderClient {
|
|
|
30
30
|
assistant(provider: Pick<ProviderDescriptor, "appId" | "key"> & {
|
|
31
31
|
installationId?: string | null;
|
|
32
32
|
}, userToken: () => Promise<string>): Promise<AssistantClient>;
|
|
33
|
+
/** Bounded task protocol transport. The selected installation is checked on every operation. */
|
|
34
|
+
taskRequest<T>(provider: import("./tasks.js").TaskProviderRef, kind: "tasks" | "task-executor", route: string, method: string, input?: unknown, userToken?: () => Promise<string>, signal?: AbortSignal): Promise<T>;
|
|
33
35
|
/** Provider-side validation checks live grants on every request; never cache this result. */
|
|
34
36
|
validate(token: string, kind: ProviderKind, key?: string, signal?: AbortSignal): Promise<ProviderInvocation>;
|
|
35
37
|
}
|
package/dist/providers-server.js
CHANGED
|
@@ -79,6 +79,19 @@ export class ProviderClient {
|
|
|
79
79
|
return { token: fresh.token };
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
|
+
/** Bounded task protocol transport. The selected installation is checked on every operation. */
|
|
83
|
+
async taskRequest(provider, kind, route, method, input, userToken, signal) {
|
|
84
|
+
const allowed = kind === "tasks"
|
|
85
|
+
? /^(?:\/capabilities|\/tasks(?:\?[a-zA-Z0-9%=&_-]*|\/[a-zA-Z0-9-]+(?:\/actions)?)?)$/
|
|
86
|
+
: /^(?:\/options|\/executions\/[a-zA-Z0-9-]+(?:\/cancel)?)$/;
|
|
87
|
+
if (!allowed.test(route) || !["GET", "POST", "PATCH", "PUT"].includes(method))
|
|
88
|
+
throw new ProviderError("task_route_invalid", "Use a version-one task operation.", 400);
|
|
89
|
+
const binding = await this.connect({ ...provider, kind }, signal, await userToken?.());
|
|
90
|
+
return read(await fetch(`${binding.url}${route}`, { method, redirect: "error", cache: "no-store",
|
|
91
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(15_000)]) : AbortSignal.timeout(15_000),
|
|
92
|
+
headers: { authorization: `Bearer ${binding.token}`, "content-type": "application/json" },
|
|
93
|
+
body: input === undefined ? undefined : JSON.stringify(input) }));
|
|
94
|
+
}
|
|
82
95
|
/** Provider-side validation checks live grants on every request; never cache this result. */
|
|
83
96
|
validate(token, kind, key = "default", signal) {
|
|
84
97
|
return this.coreRequest("/provider/introspect", { token, kind, key }, signal);
|
package/dist/providers.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
/** Provider categories supported by Core.
|
|
2
|
-
export type ProviderKind = "speech-to-text" | "assistant" | "translation";
|
|
1
|
+
/** Provider categories supported by Core. Task execution does not expose model/profile configuration. */
|
|
2
|
+
export type ProviderKind = "speech-to-text" | "assistant" | "translation" | "tasks" | "task-executor";
|
|
3
3
|
export type ProviderDescriptor = {
|
|
4
4
|
appId: string;
|
|
5
5
|
displayName: string;
|
package/dist/sandbox-server.d.ts
CHANGED
|
@@ -1,39 +1,9 @@
|
|
|
1
1
|
import type { ProviderServerOptions } from "./providers-server.js";
|
|
2
|
-
export type SandboxSource = {
|
|
3
|
-
kind: "worktree";
|
|
4
|
-
worktreeId: string;
|
|
5
|
-
} | {
|
|
6
|
-
kind: "appArtifact";
|
|
7
|
-
artifactDigest?: string;
|
|
8
|
-
};
|
|
9
|
-
export interface SandboxLimits {
|
|
10
|
-
memoryBytes?: number;
|
|
11
|
-
cpus?: number;
|
|
12
|
-
pids?: number;
|
|
13
|
-
dataBytes?: number;
|
|
14
|
-
cacheBytes?: number;
|
|
15
|
-
startupSeconds?: number;
|
|
16
|
-
}
|
|
17
2
|
export interface SandboxContext {
|
|
18
3
|
workspaceId?: string;
|
|
19
4
|
sessionId?: string;
|
|
20
5
|
jobId?: string;
|
|
21
6
|
}
|
|
22
|
-
export interface SandboxPlanInput {
|
|
23
|
-
appId: string;
|
|
24
|
-
profile: string;
|
|
25
|
-
source: SandboxSource;
|
|
26
|
-
limits?: SandboxLimits;
|
|
27
|
-
context?: SandboxContext;
|
|
28
|
-
}
|
|
29
|
-
export interface SandboxPlan {
|
|
30
|
-
id: string;
|
|
31
|
-
digest: string;
|
|
32
|
-
inputDigest: string;
|
|
33
|
-
request: SandboxPlanInput;
|
|
34
|
-
blockers: string[];
|
|
35
|
-
expiresAt: string;
|
|
36
|
-
}
|
|
37
7
|
export interface SandboxOperation {
|
|
38
8
|
id: string;
|
|
39
9
|
kind: string;
|
|
@@ -41,96 +11,11 @@ export interface SandboxOperation {
|
|
|
41
11
|
errorCode?: string;
|
|
42
12
|
message?: string;
|
|
43
13
|
}
|
|
44
|
-
export interface SandboxUser {
|
|
45
|
-
id: string;
|
|
46
|
-
fixtureKey: string;
|
|
47
|
-
displayName: string;
|
|
48
|
-
role: string;
|
|
49
|
-
permissions: string[];
|
|
50
|
-
enabled: boolean;
|
|
51
|
-
revision: number;
|
|
52
|
-
}
|
|
53
|
-
export interface SandboxInstance {
|
|
54
|
-
id: string;
|
|
55
|
-
appId: string;
|
|
56
|
-
source: SandboxSource;
|
|
57
|
-
context?: SandboxContext;
|
|
58
|
-
revision: number;
|
|
59
|
-
generation: string;
|
|
60
|
-
dataRevision: string;
|
|
61
|
-
state: string;
|
|
62
|
-
previewUrl?: string;
|
|
63
|
-
errorCode?: string;
|
|
64
|
-
operations: SandboxOperation[];
|
|
65
|
-
users: SandboxUser[];
|
|
66
|
-
viewers?: string[];
|
|
67
|
-
endpoints?: {
|
|
68
|
-
key: string;
|
|
69
|
-
url: string;
|
|
70
|
-
}[];
|
|
71
|
-
metrics?: {
|
|
72
|
-
at: string;
|
|
73
|
-
memoryBytes: number | null;
|
|
74
|
-
sampledPeakMemoryBytes: number | null;
|
|
75
|
-
services: {
|
|
76
|
-
service: string;
|
|
77
|
-
memoryBytes: number | null;
|
|
78
|
-
cpuPercent: number | null;
|
|
79
|
-
}[];
|
|
80
|
-
};
|
|
81
|
-
storage?: {
|
|
82
|
-
at: string;
|
|
83
|
-
dataBytes: number;
|
|
84
|
-
seedBytes: number;
|
|
85
|
-
cacheBytes: number;
|
|
86
|
-
evidenceBytes: number;
|
|
87
|
-
totalBytes: number;
|
|
88
|
-
warning: boolean;
|
|
89
|
-
errorCode?: string;
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export interface SandboxDiagnostics {
|
|
93
|
-
instanceId: string;
|
|
94
|
-
state: string;
|
|
95
|
-
errorCode?: string;
|
|
96
|
-
services: {
|
|
97
|
-
service: string;
|
|
98
|
-
state: string;
|
|
99
|
-
text: string;
|
|
100
|
-
}[];
|
|
101
|
-
}
|
|
102
|
-
export declare const sandboxActions: readonly ["sandbox-logs", "sandbox-plan", "sandbox-prepare", "sandbox-list", "sandbox-status", "sandbox-users", "sandbox-seed", "sandbox-start", "sandbox-restart", "sandbox-stop", "sandbox-reset", "sandbox-dispose", "sandbox-browser", "sandbox-export", "sandbox-user-delete", "sandbox-viewers", "sandbox-evidence-clear", "sandbox-preview", "sandbox-browser-cancel"];
|
|
103
|
-
export type SandboxAction = typeof sandboxActions[number];
|
|
104
14
|
export declare class SandboxError extends Error {
|
|
105
15
|
readonly code: string;
|
|
106
16
|
readonly status: number | null;
|
|
107
17
|
constructor(code: string, message: string, status: number | null);
|
|
108
18
|
}
|
|
109
|
-
/** Server-only transport. The authority callback can supply an interactive grant or an explicitly
|
|
110
|
-
* approved BackgroundGrantClient.token(); neither workspaces nor assistant roles are required. */
|
|
111
|
-
export declare class SandboxClient {
|
|
112
|
-
private readonly authority;
|
|
113
|
-
private readonly origin;
|
|
114
|
-
private readonly appId;
|
|
115
|
-
private readonly service;
|
|
116
|
-
constructor(authority: () => Promise<string>, options?: ProviderServerOptions);
|
|
117
|
-
plan(input: SandboxPlanInput): Promise<SandboxPlan>;
|
|
118
|
-
prepare(input: {
|
|
119
|
-
requestId: string;
|
|
120
|
-
planId: string;
|
|
121
|
-
digest: string;
|
|
122
|
-
}): Promise<SandboxInstance>;
|
|
123
|
-
list(): Promise<{
|
|
124
|
-
instances: SandboxInstance[];
|
|
125
|
-
}>;
|
|
126
|
-
status(runtimeInstanceId: string): Promise<SandboxInstance>;
|
|
127
|
-
command(runtimeInstanceId: string, action: "start" | "restart" | "stop" | "reset" | "dispose", input: {
|
|
128
|
-
requestId: string;
|
|
129
|
-
expectedRevision: number;
|
|
130
|
-
seedRevision?: string;
|
|
131
|
-
}): Promise<SandboxInstance>;
|
|
132
|
-
call<T = unknown>(action: SandboxAction, input: Record<string, unknown>): Promise<T>;
|
|
133
|
-
}
|
|
134
19
|
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", "environment-initialization", "environment-initialization-resume", "environment-setup-link"];
|
|
135
20
|
export type CoreEnvironmentAction = typeof coreEnvironmentActions[number];
|
|
136
21
|
export interface CoreEnvironmentSavedConsent {
|
|
@@ -153,6 +38,7 @@ export interface CoreEnvironment {
|
|
|
153
38
|
dataRoot?: string;
|
|
154
39
|
previewUrl: string;
|
|
155
40
|
initializationRequested?: boolean;
|
|
41
|
+
coreVersion?: string | null;
|
|
156
42
|
process?: {
|
|
157
43
|
pid: number;
|
|
158
44
|
startedAt: string;
|
package/dist/sandbox-server.js
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
export const sandboxActions = ["sandbox-logs", "sandbox-plan", "sandbox-prepare", "sandbox-list", "sandbox-status", "sandbox-users", "sandbox-seed",
|
|
2
|
-
"sandbox-start", "sandbox-restart", "sandbox-stop", "sandbox-reset", "sandbox-dispose", "sandbox-browser", "sandbox-export", "sandbox-user-delete", "sandbox-viewers", "sandbox-evidence-clear", "sandbox-preview", "sandbox-browser-cancel"];
|
|
3
1
|
export class SandboxError extends Error {
|
|
4
2
|
code;
|
|
5
3
|
status;
|
|
@@ -10,89 +8,6 @@ export class SandboxError extends Error {
|
|
|
10
8
|
this.name = "SandboxError";
|
|
11
9
|
}
|
|
12
10
|
}
|
|
13
|
-
/** Server-only transport. The authority callback can supply an interactive grant or an explicitly
|
|
14
|
-
* approved BackgroundGrantClient.token(); neither workspaces nor assistant roles are required. */
|
|
15
|
-
export class SandboxClient {
|
|
16
|
-
authority;
|
|
17
|
-
origin;
|
|
18
|
-
appId;
|
|
19
|
-
service;
|
|
20
|
-
constructor(authority, options = {}) {
|
|
21
|
-
this.authority = authority;
|
|
22
|
-
this.origin = new URL(options.coreOrigin ?? process.env.HOSTY_CORE_ORIGIN ?? "http://127.0.0.1:7070");
|
|
23
|
-
this.appId = options.appId ?? process.env.HOSTY_APP_ID ?? "";
|
|
24
|
-
this.service = options.serviceToken ?? process.env.HOSTY_APP_SERVICE_TOKEN ?? "";
|
|
25
|
-
if (!this.appId || !this.service || !["http:", "https:"].includes(this.origin.protocol) || this.origin.username || this.origin.password
|
|
26
|
-
|| this.origin.pathname !== "/" || this.origin.search || this.origin.hash)
|
|
27
|
-
throw new Error("A valid Core origin and calling app service identity are required.");
|
|
28
|
-
}
|
|
29
|
-
plan(input) { return this.call("sandbox-plan", input); }
|
|
30
|
-
prepare(input) { return this.call("sandbox-prepare", input); }
|
|
31
|
-
list() { return this.call("sandbox-list", {}); }
|
|
32
|
-
status(runtimeInstanceId) { return this.call("sandbox-status", { runtimeInstanceId }); }
|
|
33
|
-
command(runtimeInstanceId, action, input) {
|
|
34
|
-
return this.call(`sandbox-${action}`, { runtimeInstanceId, ...input });
|
|
35
|
-
}
|
|
36
|
-
async call(action, input) {
|
|
37
|
-
if (!sandboxActions.includes(action))
|
|
38
|
-
throw new SandboxError("sandbox_operation_invalid", "Unknown sandbox operation.", 400);
|
|
39
|
-
const { runtimeInstanceId, ...body } = input;
|
|
40
|
-
const jobId = body.jobId;
|
|
41
|
-
const root = `/api/internal/apps/${encodeURIComponent(this.appId)}/sandbox-instances`;
|
|
42
|
-
const read = action === "sandbox-logs" || action === "sandbox-list" || action === "sandbox-status" || action === "sandbox-export" || action === "sandbox-preview";
|
|
43
|
-
let suffix = "";
|
|
44
|
-
if (action === "sandbox-plan")
|
|
45
|
-
suffix = "/plans";
|
|
46
|
-
else if (action !== "sandbox-list" && action !== "sandbox-prepare") {
|
|
47
|
-
if (typeof runtimeInstanceId !== "string" || !/^[a-f0-9]{32}$/.test(runtimeInstanceId))
|
|
48
|
-
throw new SandboxError("sandbox_id_invalid", "Use the Core-issued runtime instance ID.", 400);
|
|
49
|
-
suffix = `/${runtimeInstanceId}`;
|
|
50
|
-
if (action === "sandbox-logs")
|
|
51
|
-
suffix += "/diagnostics";
|
|
52
|
-
else if (action === "sandbox-users")
|
|
53
|
-
suffix += "/users";
|
|
54
|
-
else if (action === "sandbox-user-delete")
|
|
55
|
-
suffix += "/users/delete";
|
|
56
|
-
else if (action === "sandbox-viewers")
|
|
57
|
-
suffix += "/viewers";
|
|
58
|
-
else if (action === "sandbox-evidence-clear")
|
|
59
|
-
suffix += "/evidence/clear";
|
|
60
|
-
else if (action === "sandbox-seed")
|
|
61
|
-
suffix += "/seeds";
|
|
62
|
-
else if (action === "sandbox-browser")
|
|
63
|
-
suffix += "/browser-runs";
|
|
64
|
-
else if (action === "sandbox-browser-cancel")
|
|
65
|
-
suffix += "/browser-runs/cancel";
|
|
66
|
-
else if (action === "sandbox-preview")
|
|
67
|
-
suffix += "/preview" + (typeof body.userId === "string" ? "?userId=" + encodeURIComponent(body.userId) : "");
|
|
68
|
-
else if (action === "sandbox-export") {
|
|
69
|
-
if (typeof jobId !== "string" || !/^[a-f0-9]{32}$/.test(jobId))
|
|
70
|
-
throw new SandboxError("sandbox_job_invalid", "Select a recorded browser run.", 400);
|
|
71
|
-
suffix += `/browser-runs/${jobId}`;
|
|
72
|
-
}
|
|
73
|
-
else if (!read)
|
|
74
|
-
suffix += `/operations/${action.slice("sandbox-".length)}`;
|
|
75
|
-
}
|
|
76
|
-
if (!read && action !== "sandbox-plan" && (typeof body.requestId !== "string" || !/^[a-f0-9-]{32,36}$/i.test(body.requestId)))
|
|
77
|
-
throw new SandboxError("sandbox_request_required", "Persist a request UUID before sending a mutation. Replay the same UUID and arguments after an uncertain result.", 400);
|
|
78
|
-
const credential = await this.authority();
|
|
79
|
-
let response;
|
|
80
|
-
try {
|
|
81
|
-
response = await fetch(new URL(root + suffix, this.origin), { method: read ? "GET" : "POST", redirect: "error", cache: "no-store",
|
|
82
|
-
headers: { authorization: `Bearer ${this.service}`, "X-Hosty-User-Token": credential, "content-type": "application/json" },
|
|
83
|
-
body: read ? undefined : JSON.stringify(body), signal: AbortSignal.timeout(action === "sandbox-browser" ? 330_000 : 180_000) });
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
throw new SandboxError("sandbox_outcome_unknown", "Core could not confirm the outcome. Read the operation or replay its original request UUID and arguments.", null);
|
|
87
|
-
}
|
|
88
|
-
const result = await response.json().catch(() => null);
|
|
89
|
-
if (!result)
|
|
90
|
-
throw new SandboxError("sandbox_outcome_unknown", "Core returned no readable outcome; keep the original operation identity.", response.status);
|
|
91
|
-
if (!response.ok)
|
|
92
|
-
throw new SandboxError(result.code ?? "sandbox_failed", result.message ?? "Sandbox request failed.", response.status);
|
|
93
|
-
return result;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
11
|
export const coreEnvironmentActions = ["environment-consent", "environment-create", "environment-list", "environment-status", "environment-start",
|
|
97
12
|
"environment-stop", "environment-restart", "environment-finish", "environment-setup", "environment-api", "environment-initialization", "environment-initialization-resume", "environment-setup-link"];
|
|
98
13
|
/** Full child Core transport. Requires core.environments permissions and live administrator authority.
|
package/dist/server.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import "server-only";
|
|
2
|
-
import { type SandboxIdentityContext } from "./index.js";
|
|
3
2
|
import { type AppContinuity } from "./continuity.js";
|
|
4
3
|
import { type AppAuthProtocol } from "./app-code.js";
|
|
5
4
|
export { isValidCodeVerifier } from "./app-code.js";
|
|
@@ -28,8 +27,6 @@ export interface HostIdentity {
|
|
|
28
27
|
activeUntil?: string | null;
|
|
29
28
|
activityRequired?: boolean;
|
|
30
29
|
continuity?: AppContinuity;
|
|
31
|
-
/** Present for every identity (including real viewers) acting inside a sandbox. */
|
|
32
|
-
sandbox?: SandboxIdentityContext;
|
|
33
30
|
}
|
|
34
31
|
/** Structured detail for a failed resolution: the Core HTTP status (null when Core was
|
|
35
32
|
* never reached) plus a machine-readable code and human-readable message. Codes pass
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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 { readSandboxTarget, readSandboxIdentity } from "./index.js";
|
|
5
4
|
import { readAppContinuity } from "./continuity.js";
|
|
6
5
|
import { createHostySessionResponse } from "./session-server.js";
|
|
7
6
|
import { createHostyUiResponse } from "./ui-server.js";
|
|
@@ -68,8 +67,6 @@ export function readAppIdentityToken(headers, config) {
|
|
|
68
67
|
return token;
|
|
69
68
|
}
|
|
70
69
|
}
|
|
71
|
-
if (process.env.HOSTY_SANDBOX_PROTOCOL)
|
|
72
|
-
return headers.get(HOSTY_APP_IDENTITY_HEADER)?.trim() || readCookie(headers.get("cookie"), config.identityCookieName) || null;
|
|
73
70
|
return readCookie(headers.get("cookie"), config.identityCookieName) || headers.get(HOSTY_APP_IDENTITY_HEADER)?.trim() || null;
|
|
74
71
|
}
|
|
75
72
|
/** True when the effective request protocol is https (honouring the forwarded proto). */
|
|
@@ -122,8 +119,7 @@ export async function resolveAppSession(token, config) {
|
|
|
122
119
|
};
|
|
123
120
|
}
|
|
124
121
|
const now = Date.now();
|
|
125
|
-
|
|
126
|
-
const cached = process.env.HOSTY_SANDBOX_PROTOCOL ? undefined : revalidationCache.get(token);
|
|
122
|
+
const cached = revalidationCache.get(token);
|
|
127
123
|
if (cached) {
|
|
128
124
|
if (now - cached.cachedAt < REVALIDATION_CACHE_TTL_MS && now < cached.expiresAtMs) {
|
|
129
125
|
return { status: "active", identity: cached.identity };
|
|
@@ -215,13 +211,6 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
|
|
|
215
211
|
if (!payload || payload.active !== true || !userId) {
|
|
216
212
|
return { status: "expired" };
|
|
217
213
|
}
|
|
218
|
-
let sandbox;
|
|
219
|
-
try {
|
|
220
|
-
sandbox = readSandboxIdentity(payload, readSandboxTarget(process.env));
|
|
221
|
-
}
|
|
222
|
-
catch {
|
|
223
|
-
return { status: "forbidden", error: { status: 403, code: "sandbox_identity_mismatch", message: "The identity does not match this execution target." } };
|
|
224
|
-
}
|
|
225
214
|
const hostRole = readString(payload.hostRole);
|
|
226
215
|
const expiresAt = readString(payload.expiresAt);
|
|
227
216
|
const identity = {
|
|
@@ -229,15 +218,14 @@ async function revalidateWithCore(token, coreOrigin, serviceToken, config) {
|
|
|
229
218
|
email: readString(payload.email),
|
|
230
219
|
displayName: readString(payload.displayName),
|
|
231
220
|
hostRole,
|
|
232
|
-
appRole:
|
|
221
|
+
appRole: config.mapHostRole ? config.mapHostRole(hostRole) : hostRole,
|
|
233
222
|
expiresAt,
|
|
234
223
|
activeUntil: readString(payload.activeUntil),
|
|
235
224
|
activityRequired: payload.activityRequired === true,
|
|
236
225
|
continuity: readAppContinuity(payload.continuity),
|
|
237
|
-
...(sandbox ? { sandbox } : {}),
|
|
238
226
|
};
|
|
239
227
|
const expiresAtMs = expiresAt ? Date.parse(expiresAt) : Number.NaN;
|
|
240
|
-
if (
|
|
228
|
+
if (Number.isFinite(expiresAtMs) && expiresAtMs > Date.now()) {
|
|
241
229
|
pruneRevalidationCache();
|
|
242
230
|
revalidationCache.set(token, { identity, expiresAtMs, cachedAt: Date.now() });
|
|
243
231
|
}
|
package/dist/session-server.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { type ProviderServerOptions } from "@hosty-sdk/app/providers/server";
|
|
2
2
|
import type { AppSessionStatus, SessionRecoveryParams } from "./session-types.js";
|
|
3
|
-
import type { SandboxIdentityContext } from "./sandbox-identity.js";
|
|
4
3
|
import type { AppContinuity } from "./continuity.js";
|
|
5
4
|
/** Framework-neutral server adapter. Call with an already authenticated app session, never request data. */
|
|
6
5
|
export type HostySessionInput = {
|
|
@@ -11,7 +10,6 @@ export type HostySessionInput = {
|
|
|
11
10
|
activeUntil?: string | null;
|
|
12
11
|
activityRequired?: boolean;
|
|
13
12
|
continuity?: AppContinuity;
|
|
14
|
-
sandbox?: SandboxIdentityContext;
|
|
15
13
|
};
|
|
16
14
|
error?: {
|
|
17
15
|
code: string;
|
package/dist/session-server.js
CHANGED
|
@@ -7,20 +7,15 @@ export async function createHostySessionResponse(session, recovery, options = {}
|
|
|
7
7
|
const identity = session.identity;
|
|
8
8
|
if (options.administratorOnly && identity.hostRole !== "host.admin")
|
|
9
9
|
return Response.json({ status: "forbidden", recovery }, { headers });
|
|
10
|
-
if (identity.sandbox) {
|
|
11
|
-
// Core admits only recipes without required global permissions in this offline profile.
|
|
12
|
-
// The revalidated execution identity is authoritative; never query the installed app's
|
|
13
|
-
// permission state or advertise a global permission-review action from a test realm.
|
|
14
|
-
return Response.json({ status: "active", ...identity, recovery, hosty: { version: 1, setup: "ready" } }, { headers });
|
|
15
|
-
}
|
|
16
10
|
let setup = "unavailable";
|
|
11
|
+
let permissionWarning;
|
|
17
12
|
let notice;
|
|
18
13
|
try {
|
|
19
14
|
const permissions = await new ProviderClient({ ...options, appId: recovery.appId ?? undefined }).permissions(options.signal);
|
|
20
15
|
if (![permissions.required, permissions.optional, permissions.granted, permissions.unsupportedRequired ?? []].every(list => Array.isArray(list) && list.every(name => typeof name === "string")))
|
|
21
16
|
throw new Error("Invalid permission state.");
|
|
22
|
-
setup = permissions.unsupportedRequired?.
|
|
23
|
-
|
|
17
|
+
setup = permissions.required.some(name => !permissions.unsupportedRequired?.includes(name) && !permissions.granted.includes(name)) ? "missing" : "ready";
|
|
18
|
+
permissionWarning = permissions.unsupportedRequired?.length ? "unsupported" : undefined;
|
|
24
19
|
if (identity.hostRole === "host.admin")
|
|
25
20
|
notice = { hostRole: identity.hostRole, appId: recovery.appId ?? "",
|
|
26
21
|
corePublicOrigin: recovery.corePublicOrigin, permissions };
|
|
@@ -29,5 +24,5 @@ export async function createHostySessionResponse(session, recovery, options = {}
|
|
|
29
24
|
if (error instanceof ProviderError && error.status === 404)
|
|
30
25
|
setup = "incompatible";
|
|
31
26
|
}
|
|
32
|
-
return Response.json({ status: "active", ...identity, recovery, hosty: { version: 1, setup, notice } }, { headers });
|
|
27
|
+
return Response.json({ status: "active", ...identity, recovery, hosty: { version: 1, setup, permissionWarning, notice } }, { headers });
|
|
33
28
|
}
|