@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.7.0",
3
+ "version": "0.9.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",