@hosty-sdk/app 0.7.0 → 0.9.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/dist/embedder.d.ts +27 -0
- package/dist/embedder.js +46 -1
- package/dist/index.d.ts +35 -0
- package/dist/index.js +47 -0
- package/package.json +1 -1
package/dist/embedder.d.ts
CHANGED
|
@@ -37,6 +37,33 @@ export interface DelegatedTokenRequestIntent {
|
|
|
37
37
|
* is.
|
|
38
38
|
*/
|
|
39
39
|
export declare function parseActiveFrameDelegatedTokenRequest(event: EmbedderMessage, activeFrameWindow: unknown, activeFrameUrl: string): DelegatedTokenRequestIntent | null;
|
|
40
|
+
/** A verified ask, and the one thing its payload carries. */
|
|
41
|
+
export interface AskAssistantIntent {
|
|
42
|
+
/** Trimmed and capped here, so no embedder has to remember to do it. */
|
|
43
|
+
text: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Verifies that a `message` event is an ask-the-assistant intent from the active app frame, using
|
|
47
|
+
* the same sender checks as the pair above.
|
|
48
|
+
*
|
|
49
|
+
* The text is capped in the parser rather than by the caller: a cap is part of the contract, and an
|
|
50
|
+
* embedder that forgot it would let one app's page paste itself into the operator's draft. What the
|
|
51
|
+
* embedder must still decide is *where* the text goes — this function says an app asked, never that
|
|
52
|
+
* the assistant should send anything. Nothing here may be auto-sent.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* Verifies an attention count posted by a frame the embedder deliberately embeds.
|
|
56
|
+
*
|
|
57
|
+
* Read from the frame rather than polled by the embedder: the page that holds the session list and
|
|
58
|
+
* its event stream is the one source, and a shell asking the same question on a timer would be a
|
|
59
|
+
* second source that disagrees with the first for the length of its interval.
|
|
60
|
+
*
|
|
61
|
+
* Sender-verified like every other message here, though the payload is only a count: a wrong badge is
|
|
62
|
+
* a small harm, and a wrong badge that any page could set is a way to make an operator stop trusting
|
|
63
|
+
* the badge that matters.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parseActiveFrameAttention(event: EmbedderMessage, activeFrameWindow: unknown, activeFrameUrl: string): number | null;
|
|
66
|
+
export declare function parseActiveFrameAskAssistant(event: EmbedderMessage, activeFrameWindow: unknown, activeFrameUrl: string): AskAssistantIntent | null;
|
|
40
67
|
/**
|
|
41
68
|
* Per-app reissue throttle: a frame that keeps re-posting (e.g. it never accepts the new
|
|
42
69
|
* code) must not drive an unbounded reissue storm. One reissue per app per interval is
|
package/dist/embedder.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// you have decided to grant one, mint a token from Core with your own session and post it back
|
|
13
13
|
// to that frame's origin. Both requests are unauthenticated by design; what makes them safe is
|
|
14
14
|
// that the embedder answers from facts about its own DOM rather than from what the frame claims.
|
|
15
|
-
import { AUTH_REQUIRED_INTENT_TYPE, DELEGATED_TOKEN_REQUEST_TYPE } from "./index.js";
|
|
15
|
+
import { ASK_ASSISTANT_MAX_CHARS, ASK_ASSISTANT_TYPE, ATTENTION_TYPE, AUTH_REQUIRED_INTENT_TYPE, DELEGATED_TOKEN_REQUEST_TYPE, } from "./index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Verifies that a `message` event is a genuine auth-required intent from the active app
|
|
18
18
|
* frame. Every check is about facts only the embedder can know:
|
|
@@ -57,6 +57,51 @@ export function parseActiveFrameDelegatedTokenRequest(event, activeFrameWindow,
|
|
|
57
57
|
}
|
|
58
58
|
return { refresh: candidate.refresh === true };
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Verifies that a `message` event is an ask-the-assistant intent from the active app frame, using
|
|
62
|
+
* the same sender checks as the pair above.
|
|
63
|
+
*
|
|
64
|
+
* The text is capped in the parser rather than by the caller: a cap is part of the contract, and an
|
|
65
|
+
* embedder that forgot it would let one app's page paste itself into the operator's draft. What the
|
|
66
|
+
* embedder must still decide is *where* the text goes — this function says an app asked, never that
|
|
67
|
+
* the assistant should send anything. Nothing here may be auto-sent.
|
|
68
|
+
*/
|
|
69
|
+
/**
|
|
70
|
+
* Verifies an attention count posted by a frame the embedder deliberately embeds.
|
|
71
|
+
*
|
|
72
|
+
* Read from the frame rather than polled by the embedder: the page that holds the session list and
|
|
73
|
+
* its event stream is the one source, and a shell asking the same question on a timer would be a
|
|
74
|
+
* second source that disagrees with the first for the length of its interval.
|
|
75
|
+
*
|
|
76
|
+
* Sender-verified like every other message here, though the payload is only a count: a wrong badge is
|
|
77
|
+
* a small harm, and a wrong badge that any page could set is a way to make an operator stop trusting
|
|
78
|
+
* the badge that matters.
|
|
79
|
+
*/
|
|
80
|
+
export function parseActiveFrameAttention(event, activeFrameWindow, activeFrameUrl) {
|
|
81
|
+
if (!isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl)) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const candidate = event.data;
|
|
85
|
+
// `typeof NaN === "number"`, and NaN survives every clamp below to arrive as a badge count no
|
|
86
|
+
// renderer can do anything with. Finiteness is the check that was actually meant.
|
|
87
|
+
if (candidate.type !== ATTENTION_TYPE || !Number.isFinite(candidate.count)) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
// Clamped rather than trusted: a negative or absurd count would render as a badge nobody can
|
|
91
|
+
// explain, and the frame has no business deciding how large a number the shell displays.
|
|
92
|
+
return Math.max(0, Math.min(99, Math.floor(candidate.count)));
|
|
93
|
+
}
|
|
94
|
+
export function parseActiveFrameAskAssistant(event, activeFrameWindow, activeFrameUrl) {
|
|
95
|
+
if (!isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl)) {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
const candidate = event.data;
|
|
99
|
+
if (candidate.type !== ASK_ASSISTANT_TYPE || typeof candidate.text !== "string") {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
const text = candidate.text.trim().slice(0, ASK_ASSISTANT_MAX_CHARS);
|
|
103
|
+
return text ? { text } : null;
|
|
104
|
+
}
|
|
60
105
|
/** The sender half both parsers share: right frame, right origin, object payload. */
|
|
61
106
|
function isActiveFrameMessage(event, activeFrameWindow, activeFrameUrl) {
|
|
62
107
|
if (!activeFrameWindow || event.source !== activeFrameWindow) {
|
package/dist/index.d.ts
CHANGED
|
@@ -21,6 +21,41 @@ export declare function createAuthRequiredIntent(appId: string): AuthRequiredInt
|
|
|
21
21
|
* token is a user-scoped credential, so an embedder answers only for apps it deliberately grants
|
|
22
22
|
* one (Hosty Shell: the assistant gateway, which it already mints tokens for).
|
|
23
23
|
*/
|
|
24
|
+
/**
|
|
25
|
+
* An embedded app asking its embedder to put text in the assistant's draft.
|
|
26
|
+
*
|
|
27
|
+
* The embedder reveals its assistant surface and forwards the text there; the assistant fills the
|
|
28
|
+
* **draft** and stops. Nothing is sent. That is the contract, not a UX choice: app-provided text
|
|
29
|
+
* entering a model that holds host shell is the same trust boundary that keeps MCP providers off by
|
|
30
|
+
* default, and an error message is exactly the shape a prompt injection arrives in. The operator
|
|
31
|
+
* reads it, with its source shown, and decides.
|
|
32
|
+
*
|
|
33
|
+
* Plain text only, and no reply. A structured payload would invite apps to smuggle shape the
|
|
34
|
+
* operator cannot read at a glance, and an answer would turn a hand-off into a channel.
|
|
35
|
+
*/
|
|
36
|
+
export declare const ASK_ASSISTANT_TYPE = "hosty:ask-assistant";
|
|
37
|
+
/**
|
|
38
|
+
* How many of an embedded assistant's sessions are waiting for the operator.
|
|
39
|
+
*
|
|
40
|
+
* Published by the page that holds them, not asked for on a timer: one source, so a shell's badge and
|
|
41
|
+
* the list behind it can never disagree.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ATTENTION_TYPE = "hosty:assistant-attention";
|
|
44
|
+
/** Longer than a prompt fragment is a page dumping itself into someone's draft. */
|
|
45
|
+
export declare const ASK_ASSISTANT_MAX_CHARS = 4000;
|
|
46
|
+
export interface AskAssistantMessage {
|
|
47
|
+
type: typeof ASK_ASSISTANT_TYPE;
|
|
48
|
+
/** Trimmed and capped by the embedder's parser; the app need not pre-truncate. */
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Asks the embedder to put `text` in the operator's assistant draft.
|
|
53
|
+
*
|
|
54
|
+
* Safe to call unconditionally: standalone there is no embedder to hear it, and an embedder that
|
|
55
|
+
* does not offer an assistant simply ignores it. Returns whether the message could be posted at
|
|
56
|
+
* all — never whether anyone acted on it, which is the operator's business and not the app's.
|
|
57
|
+
*/
|
|
58
|
+
export declare function askAssistant(text: string): boolean;
|
|
24
59
|
export declare const DELEGATED_TOKEN_REQUEST_TYPE = "hosty:request-delegated-token";
|
|
25
60
|
export declare const DELEGATED_TOKEN_TYPE = "hosty:delegated-token";
|
|
26
61
|
export interface DelegatedTokenRequest {
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,53 @@ export function createAuthRequiredIntent(appId) {
|
|
|
24
24
|
* token is a user-scoped credential, so an embedder answers only for apps it deliberately grants
|
|
25
25
|
* one (Hosty Shell: the assistant gateway, which it already mints tokens for).
|
|
26
26
|
*/
|
|
27
|
+
/**
|
|
28
|
+
* An embedded app asking its embedder to put text in the assistant's draft.
|
|
29
|
+
*
|
|
30
|
+
* The embedder reveals its assistant surface and forwards the text there; the assistant fills the
|
|
31
|
+
* **draft** and stops. Nothing is sent. That is the contract, not a UX choice: app-provided text
|
|
32
|
+
* entering a model that holds host shell is the same trust boundary that keeps MCP providers off by
|
|
33
|
+
* default, and an error message is exactly the shape a prompt injection arrives in. The operator
|
|
34
|
+
* reads it, with its source shown, and decides.
|
|
35
|
+
*
|
|
36
|
+
* Plain text only, and no reply. A structured payload would invite apps to smuggle shape the
|
|
37
|
+
* operator cannot read at a glance, and an answer would turn a hand-off into a channel.
|
|
38
|
+
*/
|
|
39
|
+
export const ASK_ASSISTANT_TYPE = "hosty:ask-assistant";
|
|
40
|
+
/**
|
|
41
|
+
* How many of an embedded assistant's sessions are waiting for the operator.
|
|
42
|
+
*
|
|
43
|
+
* Published by the page that holds them, not asked for on a timer: one source, so a shell's badge and
|
|
44
|
+
* the list behind it can never disagree.
|
|
45
|
+
*/
|
|
46
|
+
export const ATTENTION_TYPE = "hosty:assistant-attention";
|
|
47
|
+
/** Longer than a prompt fragment is a page dumping itself into someone's draft. */
|
|
48
|
+
export const ASK_ASSISTANT_MAX_CHARS = 4000;
|
|
49
|
+
/**
|
|
50
|
+
* Asks the embedder to put `text` in the operator's assistant draft.
|
|
51
|
+
*
|
|
52
|
+
* Safe to call unconditionally: standalone there is no embedder to hear it, and an embedder that
|
|
53
|
+
* does not offer an assistant simply ignores it. Returns whether the message could be posted at
|
|
54
|
+
* all — never whether anyone acted on it, which is the operator's business and not the app's.
|
|
55
|
+
*/
|
|
56
|
+
export function askAssistant(text) {
|
|
57
|
+
if (typeof window === "undefined" || window.parent === window) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
const trimmed = text.trim();
|
|
61
|
+
if (!trimmed) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
// Broadcast: the message carries no secret, and the embedder verifies the sender against its own
|
|
65
|
+
// DOM rather than trusting anything claimed here.
|
|
66
|
+
//
|
|
67
|
+
// Capped on the way out too. The parser remains where the contract is enforced — an app is not
|
|
68
|
+
// trusted to have done this — but there is no reason to structure-clone a megabyte across the
|
|
69
|
+
// boundary just to have the embedder discard it.
|
|
70
|
+
const capped = trimmed.slice(0, ASK_ASSISTANT_MAX_CHARS);
|
|
71
|
+
window.parent.postMessage({ type: ASK_ASSISTANT_TYPE, text: capped }, "*");
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
27
74
|
export const DELEGATED_TOKEN_REQUEST_TYPE = "hosty:request-delegated-token";
|
|
28
75
|
export const DELEGATED_TOKEN_TYPE = "hosty:delegated-token";
|
|
29
76
|
export function createDelegatedTokenRequest(options = {}) {
|
package/package.json
CHANGED