@hosty-sdk/app 0.16.2 → 0.17.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 +16 -0
- package/dist/assistant.js +1 -1
- package/dist/install.d.ts +10 -0
- package/dist/providers-server.d.ts +30 -0
- package/dist/providers-server.js +78 -0
- package/dist/providers.d.ts +43 -0
- package/dist/providers.js +10 -0
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -14,6 +14,8 @@ npm install @hosty-sdk/app
|
|
|
14
14
|
| `@hosty-sdk/app/server` | server only | Core revalidation with caching, cookie helpers, the app-code route factory, the app secrets client |
|
|
15
15
|
| `@hosty-sdk/app/react` | client | `<AppIdentityBridge />` — probe, silent recovery, fallback cards |
|
|
16
16
|
| `@hosty-sdk/app/embedder` | client | verified responders — launch-code recovery and delegated tokens — plus the theme sender half, for anything that embeds Hosty apps |
|
|
17
|
+
| `@hosty-sdk/app/providers` | anywhere | permission state, provider descriptors, speech contract types |
|
|
18
|
+
| `@hosty-sdk/app/providers/server` | server only | `ProviderClient`: discovery, speech recognition, assistant handoffs and live credential validation |
|
|
17
19
|
| `@hosty-sdk/app/theme` | anywhere | the shell→app theme protocol: constants, `resolveTheme`, `applyTheme`, `parseShellThemeMessage`, `themeBootstrapScript` / `createThemeBootstrapScript` |
|
|
18
20
|
|
|
19
21
|
Minimal Next.js wiring:
|
|
@@ -205,3 +207,17 @@ capability and raw upload API. See [the complete contract](../../docs/features/h
|
|
|
205
207
|
|
|
206
208
|
Assistant control requests use a 60-second deadline. Uploads accept an optional caller-owned
|
|
207
209
|
`AbortSignal` and have no SDK-imposed deadline, so slow valid transfers are not aborted after a minute.
|
|
210
|
+
|
|
211
|
+
## Speech And Assistant Providers
|
|
212
|
+
|
|
213
|
+
Core 0.115.0 supports required `corePermissions` and optional `optionalCorePermissions` with
|
|
214
|
+
`providers.speech-to-text` / `providers.assistant`. Each accepted permission covers all confirmed
|
|
215
|
+
providers of that category. Read `new ProviderClient().permissions()` before offering optional UI.
|
|
216
|
+
Select an app ID and interface key explicitly, use `list("speech-to-text")`, `speechCapabilities(...)`
|
|
217
|
+
and `transcribe(selected, wavBlob, { signal })` on the server. Never expose the app service token to
|
|
218
|
+
a browser. Transcription accepts the provider's documented format; the v1 baseline is 16 kHz mono
|
|
219
|
+
PCM16 WAV. Browser microphone consent is separate from Core permission.
|
|
220
|
+
|
|
221
|
+
`assistant(selected, getUserToken)` returns the existing `AssistantClient` with bounded provider
|
|
222
|
+
credentials and the acting user's access. It does not expose general agent execution.
|
|
223
|
+
See [Provider consumption](../../docs/features/provider-consumption/feature.md) for the full contract.
|
package/dist/assistant.js
CHANGED
|
@@ -51,7 +51,7 @@ export class AssistantClient {
|
|
|
51
51
|
headers.set("authorization", `Bearer ${grant.token}`);
|
|
52
52
|
if (typeof init.body === "string")
|
|
53
53
|
headers.set("content-type", "application/json");
|
|
54
|
-
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}${route}`, { ...init, headers, signal: init.signal ?? (controlDeadline ? AbortSignal.timeout(60_000) : undefined) });
|
|
54
|
+
const response = await fetch(`${this.baseUrl.replace(/\/$/, "")}${route}`, { ...init, redirect: "error", headers, signal: init.signal ?? (controlDeadline ? AbortSignal.timeout(60_000) : undefined) });
|
|
55
55
|
if (response.status === 401 && attempt === 0)
|
|
56
56
|
continue;
|
|
57
57
|
const body = await response.json().catch(() => null);
|
package/dist/install.d.ts
CHANGED
|
@@ -26,6 +26,7 @@ export interface InstallPlan {
|
|
|
26
26
|
defaultAutostart?: boolean;
|
|
27
27
|
system?: boolean;
|
|
28
28
|
corePermissions?: string[];
|
|
29
|
+
optionalCorePermissions?: string[];
|
|
29
30
|
requestedRoles?: string[];
|
|
30
31
|
permissionDescriptions?: Record<string, string>;
|
|
31
32
|
roleDescriptions?: Record<string, string>;
|
|
@@ -44,11 +45,20 @@ export interface InstallationSource {
|
|
|
44
45
|
selectedRuntime?: string;
|
|
45
46
|
updateAppId?: string;
|
|
46
47
|
planDigest?: string;
|
|
48
|
+
/** Review optional grants for this installed app, without reinstalling it. */
|
|
49
|
+
permissionsAppId?: string;
|
|
47
50
|
}
|
|
48
51
|
export interface InstallationRequest {
|
|
49
52
|
id: string;
|
|
50
53
|
status: "draft" | "pending" | "executing" | "succeeded" | "denied" | "failed";
|
|
51
54
|
plan: InstallPlan | null;
|
|
55
|
+
permissionPlan?: {
|
|
56
|
+
appId: string;
|
|
57
|
+
displayName: string;
|
|
58
|
+
required: string[];
|
|
59
|
+
optional: string[];
|
|
60
|
+
granted: string[];
|
|
61
|
+
} | null;
|
|
52
62
|
approvalUrl: string;
|
|
53
63
|
expiresAt: string;
|
|
54
64
|
error?: string | null;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AssistantClient } from "./assistant.js";
|
|
2
|
+
import { type AppPermissionState, type ProviderDescriptor, type ProviderInvocation, type ProviderKind, type SpeechCapabilities, type SpeechResult } from "./providers.js";
|
|
3
|
+
export type ProviderServerOptions = {
|
|
4
|
+
appId?: string;
|
|
5
|
+
coreOrigin?: string;
|
|
6
|
+
serviceToken?: string;
|
|
7
|
+
/** The consumer's deployment topology; endpoints on Core loopback must be reached through its host. */
|
|
8
|
+
runningInContainer?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Server-only provider client. Its app service token must never be sent to a browser or provider. */
|
|
11
|
+
export declare class ProviderClient {
|
|
12
|
+
private readonly appId;
|
|
13
|
+
private readonly core;
|
|
14
|
+
private readonly service;
|
|
15
|
+
private readonly container;
|
|
16
|
+
constructor(options?: ProviderServerOptions);
|
|
17
|
+
private coreRequest;
|
|
18
|
+
permissions(signal?: AbortSignal): Promise<AppPermissionState>;
|
|
19
|
+
list(kind: ProviderKind, signal?: AbortSignal): Promise<ProviderDescriptor[]>;
|
|
20
|
+
private connect;
|
|
21
|
+
speechCapabilities(provider: Pick<ProviderDescriptor, "appId" | "key">, signal?: AbortSignal): Promise<SpeechCapabilities>;
|
|
22
|
+
transcribe(provider: Pick<ProviderDescriptor, "appId" | "key">, audio: Blob, options?: {
|
|
23
|
+
language?: string;
|
|
24
|
+
signal?: AbortSignal;
|
|
25
|
+
}): Promise<SpeechResult>;
|
|
26
|
+
/** User-attributed requests preserve the assistant's existing handoff and dispatch policy. */
|
|
27
|
+
assistant(provider: Pick<ProviderDescriptor, "appId" | "key">, userToken: () => Promise<string>): Promise<AssistantClient>;
|
|
28
|
+
/** Provider-side validation checks live grants on every request; never cache this result. */
|
|
29
|
+
validate(token: string, kind: ProviderKind, key?: string, signal?: AbortSignal): Promise<ProviderInvocation>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { AssistantClient } from "./assistant.js";
|
|
2
|
+
import { ProviderError } from "./providers.js";
|
|
3
|
+
/** Server-only provider client. Its app service token must never be sent to a browser or provider. */
|
|
4
|
+
export class ProviderClient {
|
|
5
|
+
appId;
|
|
6
|
+
core;
|
|
7
|
+
service;
|
|
8
|
+
container;
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.appId = options.appId ?? process.env.HOSTY_APP_ID ?? "";
|
|
11
|
+
this.core = new URL(options.coreOrigin ?? process.env.HOSTY_CORE_ORIGIN ?? "http://127.0.0.1:7070");
|
|
12
|
+
this.service = options.serviceToken ?? process.env.HOSTY_APP_SERVICE_TOKEN ?? "";
|
|
13
|
+
this.container = options.runningInContainer ?? process.env.HOSTY_RUNTIME_TYPE === "docker";
|
|
14
|
+
if (!this.appId || !this.service)
|
|
15
|
+
throw new ProviderError("hosty_misconfigured", "Hosty app credentials are not configured.", 503);
|
|
16
|
+
}
|
|
17
|
+
async coreRequest(suffix, body, signal, userToken) {
|
|
18
|
+
const response = await fetch(new URL(`/api/internal/apps/${encodeURIComponent(this.appId)}${suffix}`, this.core), {
|
|
19
|
+
method: body === undefined ? "GET" : "POST", redirect: "error", cache: "no-store",
|
|
20
|
+
signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000),
|
|
21
|
+
headers: { authorization: `Bearer ${this.service}`, "content-type": "application/json", ...(userToken ? { "X-Hosty-User-Token": userToken } : {}) },
|
|
22
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
23
|
+
});
|
|
24
|
+
return read(response);
|
|
25
|
+
}
|
|
26
|
+
permissions(signal) { return this.coreRequest("/permissions", undefined, signal); }
|
|
27
|
+
async list(kind, signal) {
|
|
28
|
+
return (await this.coreRequest(`/providers/${kind}`, undefined, signal)).providers;
|
|
29
|
+
}
|
|
30
|
+
async connect(provider, signal, userToken) {
|
|
31
|
+
const issued = await this.coreRequest(`/providers/${provider.kind}/token`, { providerAppId: provider.appId, key: provider.key }, signal, userToken);
|
|
32
|
+
if (issued.provider.version !== 1 || !issued.provider.url)
|
|
33
|
+
throw new ProviderError("provider_incompatible", "This provider does not support interface version 1.", 409);
|
|
34
|
+
const url = new URL(issued.provider.url);
|
|
35
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password)
|
|
36
|
+
throw new ProviderError("provider_url_invalid", "Invalid provider endpoint.", 409);
|
|
37
|
+
// Core's internal origin already identifies the host from this consumer's vantage point.
|
|
38
|
+
if ((this.container || this.core.hostname === "host.docker.internal") && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname))
|
|
39
|
+
url.hostname = "host.docker.internal";
|
|
40
|
+
return { ...issued, url: url.href.replace(/\/$/, "") };
|
|
41
|
+
}
|
|
42
|
+
async speechCapabilities(provider, signal) {
|
|
43
|
+
const binding = await this.connect({ ...provider, kind: "speech-to-text" }, signal);
|
|
44
|
+
return read(await fetch(`${binding.url}/capabilities`, { redirect: "error", signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(10_000)]) : AbortSignal.timeout(10_000), headers: { authorization: `Bearer ${binding.token}` } }));
|
|
45
|
+
}
|
|
46
|
+
async transcribe(provider, audio, options = {}) {
|
|
47
|
+
const signal = options.signal ? AbortSignal.any([options.signal, AbortSignal.timeout(300_000)]) : AbortSignal.timeout(300_000);
|
|
48
|
+
const binding = await this.connect({ ...provider, kind: "speech-to-text" }, signal);
|
|
49
|
+
const url = new URL(`${binding.url}/transcriptions`);
|
|
50
|
+
if (options.language)
|
|
51
|
+
url.searchParams.set("language", options.language);
|
|
52
|
+
return read(await fetch(url, { method: "POST", redirect: "error", signal,
|
|
53
|
+
headers: { authorization: `Bearer ${binding.token}`, "content-type": audio.type || "audio/wav" }, body: audio }));
|
|
54
|
+
}
|
|
55
|
+
/** User-attributed requests preserve the assistant's existing handoff and dispatch policy. */
|
|
56
|
+
async assistant(provider, userToken) {
|
|
57
|
+
const target = { ...provider, kind: "assistant" };
|
|
58
|
+
const binding = await this.connect(target, undefined, await userToken());
|
|
59
|
+
return new AssistantClient(binding.url, { version: binding.provider.version, capabilities: binding.provider.capabilities }, async () => {
|
|
60
|
+
const fresh = await this.connect(target, undefined, await userToken());
|
|
61
|
+
if (fresh.url !== binding.url)
|
|
62
|
+
throw new ProviderError("provider_changed", "The assistant endpoint changed. Reconnect explicitly.", 409);
|
|
63
|
+
return { token: fresh.token };
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/** Provider-side validation checks live grants on every request; never cache this result. */
|
|
67
|
+
validate(token, kind, key = "default", signal) {
|
|
68
|
+
return this.coreRequest("/provider/introspect", { token, kind, key }, signal);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function read(response) {
|
|
72
|
+
const body = await response.json().catch(() => null);
|
|
73
|
+
if (!response.ok)
|
|
74
|
+
throw new ProviderError(body?.code ?? "provider_failed", body?.message ?? `Provider request failed (${response.status}).`, response.status);
|
|
75
|
+
if (body === null)
|
|
76
|
+
throw new ProviderError("provider_response_invalid", "Provider returned an invalid response.", 502);
|
|
77
|
+
return body;
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Provider categories supported by Core. Agent execution is intentionally not part of this contract. */
|
|
2
|
+
export type ProviderKind = "speech-to-text" | "assistant";
|
|
3
|
+
export type ProviderDescriptor = {
|
|
4
|
+
appId: string;
|
|
5
|
+
displayName: string;
|
|
6
|
+
kind: ProviderKind;
|
|
7
|
+
key: string;
|
|
8
|
+
version: number | null;
|
|
9
|
+
capabilities: string[];
|
|
10
|
+
url: string | null;
|
|
11
|
+
available: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type AppPermissionState = {
|
|
14
|
+
required: string[];
|
|
15
|
+
optional: string[];
|
|
16
|
+
granted: string[];
|
|
17
|
+
};
|
|
18
|
+
export type ProviderInvocation = {
|
|
19
|
+
callerAppId: string;
|
|
20
|
+
callerInstallation: string;
|
|
21
|
+
userId: string | null;
|
|
22
|
+
hostRole: string | null;
|
|
23
|
+
kind: ProviderKind;
|
|
24
|
+
key: string;
|
|
25
|
+
};
|
|
26
|
+
export type SpeechResult = {
|
|
27
|
+
text: string;
|
|
28
|
+
language?: string | null;
|
|
29
|
+
};
|
|
30
|
+
export type SpeechCapabilities = {
|
|
31
|
+
version: 1;
|
|
32
|
+
mediaTypes: string[];
|
|
33
|
+
maxBytes: number;
|
|
34
|
+
maxDurationSeconds: number;
|
|
35
|
+
backend: string;
|
|
36
|
+
ready: boolean;
|
|
37
|
+
};
|
|
38
|
+
/** Errors retain Core/provider codes so consumers can distinguish denial from unavailable service. */
|
|
39
|
+
export declare class ProviderError extends Error {
|
|
40
|
+
readonly code: string;
|
|
41
|
+
readonly status: number;
|
|
42
|
+
constructor(code: string, message: string, status: number);
|
|
43
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Errors retain Core/provider codes so consumers can distinguish denial from unavailable service. */
|
|
2
|
+
export class ProviderError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
status;
|
|
5
|
+
constructor(code, message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
this.status = status;
|
|
9
|
+
}
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hosty-sdk/app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0",
|
|
4
4
|
"description": "Hosty app auth: session state machine, recovery bridge, Core revalidation, and the embedder responder.",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"type": "module",
|
|
@@ -52,6 +52,14 @@
|
|
|
52
52
|
"./assistant": {
|
|
53
53
|
"types": "./dist/assistant.d.ts",
|
|
54
54
|
"default": "./dist/assistant.js"
|
|
55
|
+
},
|
|
56
|
+
"./providers": {
|
|
57
|
+
"types": "./dist/providers.d.ts",
|
|
58
|
+
"default": "./dist/providers.js"
|
|
59
|
+
},
|
|
60
|
+
"./providers/server": {
|
|
61
|
+
"types": "./dist/providers-server.d.ts",
|
|
62
|
+
"default": "./dist/providers-server.js"
|
|
55
63
|
}
|
|
56
64
|
},
|
|
57
65
|
"scripts": {
|