@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.
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ export class FeedbackError extends Error {
2
+ code;
3
+ status;
4
+ constructor(code, message, status) {
5
+ super(message);
6
+ this.code = code;
7
+ this.status = status;
8
+ }
9
+ }
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
- }
@@ -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 === "unsupported"
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 = `
@@ -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?.length)
8
- return "unsupported";
9
- return p.required.some(name => !p.granted.includes(name)) ? "missing" : null;
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
  }
@@ -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);
@@ -1,5 +1,5 @@
1
- /** Provider categories supported by Core. Agent execution is intentionally not part of this contract. */
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;
@@ -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;
@@ -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
- // Synthetic subject changes and instance revocation are checked online on every request.
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: sandbox?.identityRealm === "sandbox" ? sandbox.appRole : config.mapHostRole ? config.mapHostRole(hostRole) : hostRole,
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 (!sandbox && Number.isFinite(expiresAtMs) && expiresAtMs > Date.now()) {
228
+ if (Number.isFinite(expiresAtMs) && expiresAtMs > Date.now()) {
241
229
  pruneRevalidationCache();
242
230
  revalidationCache.set(token, { identity, expiresAtMs, cachedAt: Date.now() });
243
231
  }
@@ -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;
@@ -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?.length ? "unsupported"
23
- : permissions.required.some(name => !permissions.granted.includes(name)) ? "missing" : "ready";
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
  }