@hosty-sdk/app 0.18.0 → 0.20.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 +54 -32
- package/dist/activity-react.d.ts +7 -0
- package/dist/activity-react.js +44 -0
- package/dist/assistant.d.ts +6 -0
- package/dist/assistant.js +11 -1
- package/dist/auth-notice.d.ts +9 -0
- package/dist/auth-notice.js +48 -0
- package/dist/browser-auth.d.ts +31 -0
- package/dist/browser-auth.js +217 -0
- package/dist/embedder.d.ts +9 -1
- package/dist/embedder.js +20 -5
- package/dist/index.d.ts +11 -12
- package/dist/index.js +7 -7
- package/dist/install-react.js +1 -1
- package/dist/install.d.ts +62 -1
- package/dist/install.js +3 -2
- package/dist/permission-types.d.ts +7 -0
- package/dist/permission-types.js +1 -0
- package/dist/permissions-react.d.ts +4 -0
- package/dist/permissions-react.js +48 -0
- package/dist/permissions-server.d.ts +6 -0
- package/dist/permissions-server.js +13 -0
- package/dist/permissions.d.ts +8 -0
- package/dist/permissions.js +25 -0
- package/dist/providers-server.d.ts +2 -2
- package/dist/providers-server.js +2 -2
- package/dist/providers.d.ts +2 -0
- package/dist/react.d.ts +8 -2
- package/dist/react.js +139 -59
- package/dist/scoped-token.d.ts +5 -0
- package/dist/scoped-token.js +11 -1
- package/dist/server.d.ts +5 -3
- package/dist/server.js +18 -10
- package/package.json +22 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The postMessage type an embedded app
|
|
2
|
+
* The postMessage type for an embedded app's authentication-required intent.
|
|
3
3
|
* A frozen protocol constant: it deliberately does not track product branding (precedent:
|
|
4
4
|
* the `x-docker-host-identity` header survived the docker-host→hosty rename untouched).
|
|
5
5
|
* The payload carries no secret — the embedder verifies the sender before acting.
|
|
@@ -13,8 +13,8 @@ export declare function createAuthRequiredIntent(appId: string): AuthRequiredInt
|
|
|
13
13
|
/**
|
|
14
14
|
* The postMessage types of the delegated-token handshake: an embedded app page asks its embedder
|
|
15
15
|
* for a short-TTL token scoped to itself, and the embedder answers with one it minted from Core.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* Minting a delegated token uses the embedder's first-party Core session. The app's own identity
|
|
17
|
+
* sign-in is separate: it navigates directly to Core or opens its own popup.
|
|
18
18
|
*
|
|
19
19
|
* The request carries no secret, so it is safe to broadcast; the answer does, so an embedder must
|
|
20
20
|
* post it to the frame's own origin and never to `*`. Answering is a decision, not a reflex: the
|
|
@@ -126,8 +126,9 @@ export declare function buildCoreOpenUrl(corePublicOrigin: string | null, appId:
|
|
|
126
126
|
/**
|
|
127
127
|
* How a shell is presenting this app.
|
|
128
128
|
*
|
|
129
|
-
* - `embedded` — framed by the browser Shell. The shell renders navigation for this app
|
|
130
|
-
*
|
|
129
|
+
* - `embedded` — framed by the browser Shell. The shell renders navigation for this app. The
|
|
130
|
+
* app restores its per-tab grant, tries a silent Core redirect on first load, then uses its
|
|
131
|
+
* own Core popup when interaction is required. No credential passes through the parent.
|
|
131
132
|
* - `native` — the top frame inside a native shell's web view (`apps/shell-swift`). The shell
|
|
132
133
|
* renders navigation, but there is no parent to post to, so recovery takes the standalone
|
|
133
134
|
* redirect — which is exactly the navigation that client intercepts to re-mint a launch code.
|
|
@@ -135,10 +136,9 @@ export declare function buildCoreOpenUrl(corePublicOrigin: string | null, appId:
|
|
|
135
136
|
* for this app, so it keeps its own.
|
|
136
137
|
*
|
|
137
138
|
* Two independent decisions read this one value and they do not split it the same way: chrome
|
|
138
|
-
* is hidden whenever the mode is not `standalone` (`hidesAppChrome`), while
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* hidden would send recovery to a parent that is not there.
|
|
139
|
+
* is hidden whenever the mode is not `standalone` (`hidesAppChrome`), while frame-local silent
|
|
140
|
+
* sign-in and popup recovery belong to `embedded` alone (`AppIdentityBridge`). A native web
|
|
141
|
+
* view keeps standalone recovery even though its app chrome is hidden.
|
|
142
142
|
*/
|
|
143
143
|
export type AppLaunchMode = "embedded" | "native" | "standalone";
|
|
144
144
|
/** The query parameter a shell appends to the launch URL to declare the mode. */
|
|
@@ -160,9 +160,8 @@ export declare function normalizeLaunchMode(value: unknown): AppLaunchMode | nul
|
|
|
160
160
|
* `standalone` — a native shell's web view makes the app the top frame, so it reads as
|
|
161
161
|
* `standalone`, which is why the declared `hosty_launch` parameter exists at all.
|
|
162
162
|
*
|
|
163
|
-
* This stays the input to the *recovery* decision, because
|
|
164
|
-
*
|
|
165
|
-
* post into a window with no shell listening.
|
|
163
|
+
* This stays the input to the *recovery* decision, because framing is a structural fact a
|
|
164
|
+
* declared value cannot override: a stale `embedded` must not select frame-only silent sign-in.
|
|
166
165
|
*/
|
|
167
166
|
export declare function detectLaunchMode(win: Pick<Window, "self" | "top">): Exclude<AppLaunchMode, "native">;
|
|
168
167
|
export interface LaunchModeResolution {
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// This is the single source of truth for the Hosty app-session contract described in
|
|
5
5
|
// docker-host docs/features/hosty-app-sdk/feature.md and docs/features/auth-session-lifecycle/feature.md.
|
|
6
6
|
/**
|
|
7
|
-
* The postMessage type an embedded app
|
|
7
|
+
* The postMessage type for an embedded app's authentication-required intent.
|
|
8
8
|
* A frozen protocol constant: it deliberately does not track product branding (precedent:
|
|
9
9
|
* the `x-docker-host-identity` header survived the docker-host→hosty rename untouched).
|
|
10
10
|
* The payload carries no secret — the embedder verifies the sender before acting.
|
|
@@ -16,8 +16,8 @@ export function createAuthRequiredIntent(appId) {
|
|
|
16
16
|
/**
|
|
17
17
|
* The postMessage types of the delegated-token handshake: an embedded app page asks its embedder
|
|
18
18
|
* for a short-TTL token scoped to itself, and the embedder answers with one it minted from Core.
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Minting a delegated token uses the embedder's first-party Core session. The app's own identity
|
|
20
|
+
* sign-in is separate: it navigates directly to Core or opens its own popup.
|
|
21
21
|
*
|
|
22
22
|
* The request carries no secret, so it is safe to broadcast; the answer does, so an embedder must
|
|
23
23
|
* post it to the frame's own origin and never to `*`. Answering is a decision, not a reflex: the
|
|
@@ -152,9 +152,8 @@ export function normalizeLaunchMode(value) {
|
|
|
152
152
|
* `standalone` — a native shell's web view makes the app the top frame, so it reads as
|
|
153
153
|
* `standalone`, which is why the declared `hosty_launch` parameter exists at all.
|
|
154
154
|
*
|
|
155
|
-
* This stays the input to the *recovery* decision, because
|
|
156
|
-
*
|
|
157
|
-
* post into a window with no shell listening.
|
|
155
|
+
* This stays the input to the *recovery* decision, because framing is a structural fact a
|
|
156
|
+
* declared value cannot override: a stale `embedded` must not select frame-only silent sign-in.
|
|
158
157
|
*/
|
|
159
158
|
export function detectLaunchMode(win) {
|
|
160
159
|
try {
|
|
@@ -241,7 +240,8 @@ export function decideRecoveryAction(input) {
|
|
|
241
240
|
}
|
|
242
241
|
// expired / not-present — the recoverable pair.
|
|
243
242
|
//
|
|
244
|
-
// `embedded`
|
|
243
|
+
// `embedded` selects app-owned interactive recovery after the bridge's silent first-load path.
|
|
244
|
+
// `native` deliberately falls through to the
|
|
245
245
|
// redirect beside `standalone`: a native shell's web view has no parent, and the redirect to
|
|
246
246
|
// Core's `/open` is the navigation that client watches for to re-mint a launch code. Adding a
|
|
247
247
|
// mode must never quietly add a case here — a mode that cannot reach a shell has to redirect.
|
package/dist/install-react.js
CHANGED
|
@@ -64,7 +64,7 @@ export function InstallDialog({ client, source, onClose, onInstalled }) {
|
|
|
64
64
|
else
|
|
65
65
|
popup?.close();
|
|
66
66
|
};
|
|
67
|
-
return _jsxs("dialog", { ref: dialog, className: "hosty-install", "aria-labelledby": titleId, onCancel: event => { event.preventDefault(); onClose(); }, children: [_jsx("style", { children: styles }), _jsxs("header", { children: [_jsx("h2", { id: titleId, children: "Install app" }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close installation", children: "\u00D7" })] }), _jsx("p", { children: "Prepare the installation here, then confirm it securely in Hosty Core." }), !source?.feedsUrl && !source?.manifestPath && _jsxs("form", { onSubmit: event => { event.preventDefault(); void review(); }, children: [_jsxs("label", { children: ["Manifest, app directory, or URL", _jsx("input", { value: manifestPath, onChange: event => { setManifestPath(event.target.value); flow.clearReview(); }, required: true, disabled: busy || frozen })] }), _jsx("button", { disabled: busy || frozen || !manifestPath.trim(), children: "Review" })] }), busy && _jsx("p", { role: "status", children: "Preparing installation\u2026" }), error && _jsxs("div", { role: "alert", children: [_jsx("p", { children: error }), !frozen && _jsx("button", { type: "button", disabled: busy, onClick: () => void review(), children: "Retry review" })] }), plan && _jsxs("form", { onSubmit: event => { event.preventDefault(); void submit(); }, children: [_jsxs("h3", { children: [plan.displayName, " ", _jsx("small", { children: plan.targetVersion })] }), plan.description && _jsx("p", { children: plan.description }), _jsxs("label", { children: ["Runtime", _jsx("select", { value: plan.targetRuntime, disabled: busy || frozen, onChange: event => void review(event.target.value), children: (plan.runtimeProfiles ?? [{ key: plan.targetRuntime, type: plan.targetRuntimeType }]).map(runtime => _jsxs("option", { value: runtime.key, children: [runtime.key, " (", runtime.type, ")"] }, runtime.key)) })] }), plan.targetRuntimeType === "localCommand" && _jsx("p", { className: "warning", children: "This runtime runs commands directly on your host, outside a container. Only install code you trust." }), plan.system && _jsx("p", { className: "warning", children: "This is a system app.
|
|
67
|
+
return _jsxs("dialog", { ref: dialog, className: "hosty-install", "aria-labelledby": titleId, onCancel: event => { event.preventDefault(); onClose(); }, children: [_jsx("style", { children: styles }), _jsxs("header", { children: [_jsx("h2", { id: titleId, children: "Install app" }), _jsx("button", { type: "button", onClick: onClose, "aria-label": "Close installation", children: "\u00D7" })] }), _jsx("p", { children: "Prepare the installation here, then confirm it securely in Hosty Core." }), !source?.feedsUrl && !source?.manifestPath && _jsxs("form", { onSubmit: event => { event.preventDefault(); void review(); }, children: [_jsxs("label", { children: ["Manifest, app directory, or URL", _jsx("input", { value: manifestPath, onChange: event => { setManifestPath(event.target.value); flow.clearReview(); }, required: true, disabled: busy || frozen })] }), _jsx("button", { disabled: busy || frozen || !manifestPath.trim(), children: "Review" })] }), busy && _jsx("p", { role: "status", children: "Preparing installation\u2026" }), error && _jsxs("div", { role: "alert", children: [_jsx("p", { children: error }), !frozen && _jsx("button", { type: "button", disabled: busy, onClick: () => void review(), children: "Retry review" })] }), plan && _jsxs("form", { onSubmit: event => { event.preventDefault(); void submit(); }, children: [_jsxs("h3", { children: [plan.displayName, " ", _jsx("small", { children: plan.targetVersion })] }), plan.description && _jsx("p", { children: plan.description }), _jsxs("label", { children: ["Runtime", _jsx("select", { value: plan.targetRuntime, disabled: busy || frozen, onChange: event => void review(event.target.value), children: (plan.runtimeProfiles ?? [{ key: plan.targetRuntime, type: plan.targetRuntimeType }]).map(runtime => _jsxs("option", { value: runtime.key, children: [runtime.key, " (", runtime.type, ")"] }, runtime.key)) })] }), plan.targetRuntimeType === "localCommand" && _jsx("p", { className: "warning", children: "This runtime runs commands directly on your host, outside a container. Only install code you trust." }), plan.system && _jsx("p", { className: "warning", children: "This is a system app. User access is controlled by Hosty assignments." }), !!plan.requestedRoles?.length && _jsxs("section", { children: [_jsx("h3", { children: "Requested provider roles" }), _jsx("ul", { children: plan.requestedRoles.map(role => _jsx("li", { children: plan.roleDescriptions?.[role] ?? role }, role)) })] }), !!plan.corePermissions?.length && _jsxs("section", { children: [_jsx("h3", { children: "Requested Core permissions" }), _jsx("ul", { children: plan.corePermissions.map(permission => _jsx("li", { children: plan.permissionDescriptions?.[permission] ?? permission }, permission)) })] }), plan.settings.map(setting => _jsxs("label", { children: [setting.label || setting.key, setting.required ? " (required)" : "", setting.type === "boolean" && !setting.secret
|
|
68
68
|
? _jsx("input", { type: "checkbox", checked: ["true", "1", "yes", "on", "enabled"].includes((settings[setting.key] ?? "").toLowerCase()), disabled: busy || frozen, onChange: event => setSettings(current => ({ ...current, [setting.key]: String(event.target.checked) })) })
|
|
69
69
|
: setting.type === "select" && setting.options?.length && !setting.secret
|
|
70
70
|
? _jsxs("select", { value: settings[setting.key] ?? "", disabled: busy || frozen, onChange: event => setSettings(current => ({ ...current, [setting.key]: event.target.value })), children: [_jsx("option", { value: "", children: "Select a value" }), setting.options.map(option => _jsx("option", { value: option.value, children: option.label }, option.value))] })
|
package/dist/install.d.ts
CHANGED
|
@@ -47,11 +47,61 @@ export interface InstallationSource {
|
|
|
47
47
|
planDigest?: string;
|
|
48
48
|
/** Review permission declarations and optional grants for this installed app, without reinstalling it. */
|
|
49
49
|
permissionsAppId?: string;
|
|
50
|
+
/** Request removal of this installed app. Core freezes these options for its confirmation page. */
|
|
51
|
+
removeAppId?: string;
|
|
52
|
+
removalOptions?: {
|
|
53
|
+
deleteRuntimeState?: boolean;
|
|
54
|
+
deleteData?: boolean;
|
|
55
|
+
deleteBackups?: boolean;
|
|
56
|
+
deleteSource?: boolean;
|
|
57
|
+
ignoreRuntimeErrors?: boolean;
|
|
58
|
+
};
|
|
59
|
+
hostPathChange?: {
|
|
60
|
+
kind: "source-override";
|
|
61
|
+
appId: string;
|
|
62
|
+
source: {
|
|
63
|
+
path: string;
|
|
64
|
+
commit?: string;
|
|
65
|
+
};
|
|
66
|
+
} | {
|
|
67
|
+
kind: "app-mounts";
|
|
68
|
+
appId: string;
|
|
69
|
+
mounts: {
|
|
70
|
+
mounts: {
|
|
71
|
+
key: string;
|
|
72
|
+
label?: string | null;
|
|
73
|
+
hostPath?: string | null;
|
|
74
|
+
globalMountName?: string | null;
|
|
75
|
+
}[];
|
|
76
|
+
};
|
|
77
|
+
} | {
|
|
78
|
+
kind: "global-mount";
|
|
79
|
+
globalMount: {
|
|
80
|
+
name: string;
|
|
81
|
+
hostPath: string;
|
|
82
|
+
mode?: string;
|
|
83
|
+
description?: string | null;
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
/** Personal source selection requires apps.sources as well as apps.install and Core review. */
|
|
87
|
+
sourceConnections?: {
|
|
88
|
+
manifestConnectionId?: string;
|
|
89
|
+
gitConnectionId?: string;
|
|
90
|
+
clearManifestConnection?: boolean;
|
|
91
|
+
clearGitConnection?: boolean;
|
|
92
|
+
};
|
|
50
93
|
}
|
|
51
94
|
export interface InstallationRequest {
|
|
52
95
|
id: string;
|
|
53
96
|
status: "draft" | "pending" | "executing" | "succeeded" | "denied" | "failed";
|
|
54
97
|
plan: InstallPlan | null;
|
|
98
|
+
updatePlan?: {
|
|
99
|
+
appId: string;
|
|
100
|
+
displayName: string;
|
|
101
|
+
targetVersion: string;
|
|
102
|
+
targetRuntime: string;
|
|
103
|
+
changes: string[];
|
|
104
|
+
} | null;
|
|
55
105
|
permissionPlan?: {
|
|
56
106
|
appId: string;
|
|
57
107
|
displayName: string;
|
|
@@ -61,13 +111,24 @@ export interface InstallationRequest {
|
|
|
61
111
|
acceptedRequired?: string[];
|
|
62
112
|
acceptedOptional?: string[];
|
|
63
113
|
} | null;
|
|
114
|
+
removalPlan?: {
|
|
115
|
+
appId: string;
|
|
116
|
+
displayName: string;
|
|
117
|
+
version: string;
|
|
118
|
+
options: NonNullable<InstallationSource["removalOptions"]>;
|
|
119
|
+
} | null;
|
|
120
|
+
hostPathPlan?: {
|
|
121
|
+
change: InstallationSource["hostPathChange"];
|
|
122
|
+
displayName: string;
|
|
123
|
+
details: string[];
|
|
124
|
+
} | null;
|
|
64
125
|
approvalUrl: string;
|
|
65
126
|
expiresAt: string;
|
|
66
127
|
error?: string | null;
|
|
67
128
|
}
|
|
68
129
|
export interface InstallationClient {
|
|
69
130
|
prepare(source: InstallationSource): Promise<InstallationRequest>;
|
|
70
|
-
submit(id: string, settings: Record<string, string | null>, autostart: boolean
|
|
131
|
+
submit(id: string, settings: Record<string, string | null>, autostart: boolean): Promise<InstallationRequest>;
|
|
71
132
|
status(id: string): Promise<InstallationRequest>;
|
|
72
133
|
}
|
|
73
134
|
export declare class InstallationError extends Error {
|
package/dist/install.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { appFetch } from "./browser-auth.js";
|
|
1
2
|
export class InstallationError extends Error {
|
|
2
3
|
status;
|
|
3
4
|
code;
|
|
@@ -9,7 +10,7 @@ export class InstallationError extends Error {
|
|
|
9
10
|
}
|
|
10
11
|
export function createInstallationClient(options = {}) {
|
|
11
12
|
const base = (options.baseUrl ?? "/api/hosty/installations").replace(/\/$/, "");
|
|
12
|
-
const request = options.request ?? ((url, body, method = "POST") =>
|
|
13
|
+
const request = options.request ?? ((url, body, method = "POST") => appFetch(url, {
|
|
13
14
|
method, credentials: "same-origin", cache: "no-store",
|
|
14
15
|
headers: { "Content-Type": "application/json" },
|
|
15
16
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
@@ -25,7 +26,7 @@ export function createInstallationClient(options = {}) {
|
|
|
25
26
|
}
|
|
26
27
|
return {
|
|
27
28
|
prepare: source => call("", source),
|
|
28
|
-
submit: (id, settings, autostart
|
|
29
|
+
submit: (id, settings, autostart) => call(`/${encodeURIComponent(id)}/submit`, { settings, autostart }),
|
|
29
30
|
status: id => call(`/${encodeURIComponent(id)}`, undefined, "GET"),
|
|
30
31
|
};
|
|
31
32
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
import { appFetch } from "./browser-auth.js";
|
|
5
|
+
import { permissionNotice, requestPermissionReview } from "./permissions.js";
|
|
6
|
+
/** App-owned GET endpoint authenticates the viewer and calls readOwnPermissionNotice. */
|
|
7
|
+
export function MissingPermissionsNotice({ endpoint = "/api/hosty/permissions" }) {
|
|
8
|
+
const [state, setState] = useState(null);
|
|
9
|
+
const [dismissed, setDismissed] = useState(false);
|
|
10
|
+
const [pending, setPending] = useState(false);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
let loading = false;
|
|
14
|
+
const refresh = async () => {
|
|
15
|
+
if (loading)
|
|
16
|
+
return;
|
|
17
|
+
loading = true;
|
|
18
|
+
try {
|
|
19
|
+
const response = await appFetch(endpoint, { cache: "no-store", signal: controller.signal });
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
setState(null);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const next = await response.json();
|
|
25
|
+
if (!controller.signal.aborted) {
|
|
26
|
+
setState(next);
|
|
27
|
+
if (!permissionNotice(next))
|
|
28
|
+
setPending(false);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch { /* A transient failure must not erase a known setup warning. */ }
|
|
32
|
+
finally {
|
|
33
|
+
loading = false;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
void refresh();
|
|
37
|
+
window.addEventListener("focus", refresh);
|
|
38
|
+
const timer = pending ? window.setInterval(refresh, 2000) : undefined;
|
|
39
|
+
return () => { controller.abort(); window.removeEventListener("focus", refresh); window.clearInterval(timer); };
|
|
40
|
+
}, [endpoint, pending]);
|
|
41
|
+
const kind = state && permissionNotice(state);
|
|
42
|
+
if (dismissed || !kind || !state)
|
|
43
|
+
return null;
|
|
44
|
+
return _jsxs("section", { "data-hosty-activity-control": true, role: "alert", "aria-label": "Required permissions", style: { border: "1px solid currentColor", padding: 12, margin: 8, borderRadius: 8, color: "#c2410c" }, children: [kind === "unsupported"
|
|
45
|
+
? _jsxs("p", { children: ["Core does not support required permissions: ", state.permissions.unsupportedRequired.join(", "), ". Update the app or Core."] })
|
|
46
|
+
: _jsx("p", { children: "This app needs required permissions. Operations using them are unavailable until approval." }), kind === "missing" && state.permissions?.reviewAvailable !== false && state.corePublicOrigin &&
|
|
47
|
+
_jsx("button", { type: "button", onClick: () => { requestPermissionReview(state); setPending(true); }, children: "Request permissions" }), pending && _jsx("p", { children: "Review permissions in Core, then return here. You can leave this app installed without approving." }), _jsx("button", { type: "button", onClick: () => { setDismissed(true); setPending(false); }, style: { marginLeft: 12 }, children: "Dismiss" })] });
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type ProviderServerOptions } from "@hosty-sdk/app/providers/server";
|
|
2
|
+
import type { PermissionNoticeState } from "./permission-types.js";
|
|
3
|
+
/** Server-only: pass the role from an authenticated session, never from request input. */
|
|
4
|
+
export declare function readOwnPermissionNotice(hostRole: string | null, options?: ProviderServerOptions & {
|
|
5
|
+
corePublicOrigin?: string | null;
|
|
6
|
+
}, signal?: AbortSignal): Promise<PermissionNoticeState>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { ProviderClient } from "@hosty-sdk/app/providers/server";
|
|
2
|
+
/** Server-only: pass the role from an authenticated session, never from request input. */
|
|
3
|
+
export async function readOwnPermissionNotice(hostRole, options = {}, signal) {
|
|
4
|
+
const state = {
|
|
5
|
+
hostRole,
|
|
6
|
+
appId: options.appId ?? process.env.HOSTY_APP_ID ?? "",
|
|
7
|
+
corePublicOrigin: options.corePublicOrigin ?? process.env.HOSTY_CORE_PUBLIC_ORIGIN ?? null,
|
|
8
|
+
permissions: null,
|
|
9
|
+
};
|
|
10
|
+
if (hostRole === "host.admin")
|
|
11
|
+
state.permissions = await new ProviderClient(options).permissions(signal);
|
|
12
|
+
return state;
|
|
13
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { PermissionNoticeState } from "./permission-types.js";
|
|
2
|
+
export type { PermissionNoticeState } from "./permission-types.js";
|
|
3
|
+
export declare const PERMISSION_REVIEW_REQUEST = "hosty:request-permission-review";
|
|
4
|
+
/** No optional declaration, or declaration alone, confers authority. */
|
|
5
|
+
export declare function permissionNotice(state: PermissionNoticeState): "missing" | "unsupported" | null;
|
|
6
|
+
export declare function permissionReviewUrl(corePublicOrigin: string, appId: string): string;
|
|
7
|
+
/** Invoke synchronously from a user click. The embedder determines the app from its frame. */
|
|
8
|
+
export declare function requestPermissionReview(state: PermissionNoticeState, browser?: Pick<Window, "parent" | "open">): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const PERMISSION_REVIEW_REQUEST = "hosty:request-permission-review";
|
|
2
|
+
/** No optional declaration, or declaration alone, confers authority. */
|
|
3
|
+
export function permissionNotice(state) {
|
|
4
|
+
if (state.hostRole !== "host.admin" || !state.permissions)
|
|
5
|
+
return null;
|
|
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;
|
|
10
|
+
}
|
|
11
|
+
export function permissionReviewUrl(corePublicOrigin, appId) {
|
|
12
|
+
const core = new URL(corePublicOrigin);
|
|
13
|
+
if (!["http:", "https:"].includes(core.protocol) || core.username || core.password)
|
|
14
|
+
throw new Error("Invalid Core origin");
|
|
15
|
+
return new URL(`/install/permissions/${encodeURIComponent(appId)}`, core.origin).href;
|
|
16
|
+
}
|
|
17
|
+
/** Invoke synchronously from a user click. The embedder determines the app from its frame. */
|
|
18
|
+
export function requestPermissionReview(state, browser = window) {
|
|
19
|
+
if (permissionNotice(state) !== "missing" || state.permissions?.reviewAvailable === false || !state.corePublicOrigin)
|
|
20
|
+
return;
|
|
21
|
+
if (browser.parent !== browser)
|
|
22
|
+
browser.parent.postMessage({ type: PERMISSION_REVIEW_REQUEST }, "*");
|
|
23
|
+
else
|
|
24
|
+
browser.open(permissionReviewUrl(state.corePublicOrigin, state.appId), "_blank", "noopener,noreferrer");
|
|
25
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AssistantClient } from "
|
|
2
|
-
import { type AppPermissionState, type ProviderDescriptor, type ProviderInvocation, type ProviderKind, type SpeechCapabilities, type SpeechResult } from "
|
|
1
|
+
import { AssistantClient } from "@hosty-sdk/app/assistant";
|
|
2
|
+
import { type AppPermissionState, type ProviderDescriptor, type ProviderInvocation, type ProviderKind, type SpeechCapabilities, type SpeechResult } from "@hosty-sdk/app/providers";
|
|
3
3
|
export type ProviderServerOptions = {
|
|
4
4
|
appId?: string;
|
|
5
5
|
coreOrigin?: string;
|
package/dist/providers-server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { AssistantClient } from "
|
|
2
|
-
import { ProviderError } from "
|
|
1
|
+
import { AssistantClient } from "@hosty-sdk/app/assistant";
|
|
2
|
+
import { ProviderError } from "@hosty-sdk/app/providers";
|
|
3
3
|
/** Server-only provider client. Its app service token must never be sent to a browser or provider. */
|
|
4
4
|
export class ProviderClient {
|
|
5
5
|
appId;
|
package/dist/providers.d.ts
CHANGED
package/dist/react.d.ts
CHANGED
|
@@ -34,6 +34,8 @@ export type AppIdentityBridgeState = {
|
|
|
34
34
|
kind: "signin";
|
|
35
35
|
openUrl: string | null;
|
|
36
36
|
embedded: boolean;
|
|
37
|
+
signIn?: () => void;
|
|
38
|
+
error?: string;
|
|
37
39
|
} | {
|
|
38
40
|
kind: "denied";
|
|
39
41
|
} | {
|
|
@@ -48,15 +50,17 @@ export interface AppIdentityBridgeProps {
|
|
|
48
50
|
appCodePath?: string;
|
|
49
51
|
/** Optional app-owned rendering, including loading and active content. Recovery remains SDK-owned. */
|
|
50
52
|
renderState?: (state: AppIdentityBridgeState) => ReactNode;
|
|
53
|
+
children?: ReactNode;
|
|
51
54
|
}
|
|
52
55
|
/**
|
|
53
56
|
* Mount once at the top of the root layout body. On first load it consumes a `?code` from
|
|
54
57
|
* the URL (exchanging it for the identity cookie), then probes the session and runs the
|
|
55
|
-
* recovery decision: silent
|
|
58
|
+
* recovery decision: a silent first-load Core navigation followed by an app-owned popup
|
|
59
|
+
* when embedded, a once-per-tab
|
|
56
60
|
* redirect through Core `/open` when standalone, and cards only in fallback/terminal states
|
|
57
61
|
* — never a login UI while recovery is still running.
|
|
58
62
|
*/
|
|
59
|
-
export declare function AppIdentityBridge({ probePath, appCodePath, renderState, }?: AppIdentityBridgeProps): string | number | bigint | boolean |
|
|
63
|
+
export declare function AppIdentityBridge({ probePath, appCodePath, renderState, children, }?: AppIdentityBridgeProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react").JSX.Element | null | undefined;
|
|
60
64
|
export interface HostThemeBridgeProps {
|
|
61
65
|
/**
|
|
62
66
|
* Whether the operating system decides the theme while no shell has spoken — `true` unless the
|
|
@@ -85,3 +89,5 @@ export interface HostThemeBridgeProps {
|
|
|
85
89
|
* choice is not overruled by the OS.
|
|
86
90
|
*/
|
|
87
91
|
export declare function HostThemeBridge({ followSystem, onTheme }?: HostThemeBridgeProps): null;
|
|
92
|
+
export { MissingPermissionsNotice } from "./permissions-react.js";
|
|
93
|
+
export { AppActivityBridge } from "./activity-react.js";
|