appilot 0.0.1 → 0.1.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 +101 -7
- package/dist/dom/surfaces.d.ts +81 -0
- package/dist/dom/surfaces.js +91 -0
- package/dist/dom/uniqueSelector.d.ts +23 -0
- package/dist/dom/uniqueSelector.js +104 -0
- package/dist/domResponder.d.ts +38 -0
- package/dist/domResponder.js +344 -0
- package/dist/focusedSessions/pageSdk.d.ts +107 -0
- package/dist/focusedSessions/pageSdk.js +238 -0
- package/dist/httpFetch.d.ts +52 -0
- package/dist/httpFetch.js +137 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +24 -0
- package/dist/runtime.d.ts +15 -0
- package/dist/runtime.js +18 -0
- package/dist/sessions/types.d.ts +140 -0
- package/dist/sessions/types.js +12 -0
- package/dist/webmcp/registry.d.ts +75 -0
- package/dist/webmcp/registry.js +152 -0
- package/dist/webmcp/sdk.d.ts +38 -0
- package/dist/webmcp/sdk.js +81 -0
- package/dist/widget/boot.d.ts +72 -0
- package/dist/widget/boot.js +264 -0
- package/package.json +51 -16
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot` client actions: the WebMCP-aligned surface a host app calls to
|
|
3
|
+
* expose its actions to the Appilot agent.
|
|
4
|
+
*
|
|
5
|
+
* Two postures, one call site:
|
|
6
|
+
* - POLYFILL (provider): for the 99% of apps with no native WebMCP, our
|
|
7
|
+
* in-page registry IS the tool surface (see registry.ts).
|
|
8
|
+
* - CONSUMER (native): when the browser exposes `navigator.modelContext` /
|
|
9
|
+
* `document.modelContext`, we also delegate registration to it (so the page
|
|
10
|
+
* is a real WebMCP page for any agent), and the collector prefers native
|
|
11
|
+
* tools. Namespace is detect-only because the spec is in flux; we bind to
|
|
12
|
+
* the stable descriptor (name/description/inputSchema/execute).
|
|
13
|
+
*
|
|
14
|
+
* Import from `appilot`. See docs/content-model/tools-client-actions.md.
|
|
15
|
+
*/
|
|
16
|
+
import { type ClientToolDefinition, type ClientToolHandle, type SerializedClientTool } from './registry.js';
|
|
17
|
+
/** True when the browser natively implements a WebMCP `modelContext`. */
|
|
18
|
+
export declare function hasNativeWebMcp(): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Register a tool with Appilot, WebMCP-style. The descriptor is the stable
|
|
21
|
+
* WebMCP shape: `{ name, description, inputSchema?, annotations?, execute }`.
|
|
22
|
+
*
|
|
23
|
+
* If the browser exposes a native `modelContext`, we ALSO register there so a
|
|
24
|
+
* native browser agent sees the same tool: "re-emission, not rewrite". We
|
|
25
|
+
* always mirror into Appilot's registry so collection does not depend on the
|
|
26
|
+
* still-unspecified native `getTools()`/`executeTool()` discovery API.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registerTool(def: ClientToolDefinition): ClientToolHandle;
|
|
29
|
+
/**
|
|
30
|
+
* The per-turn collector the extension/widget call to gather the descriptors
|
|
31
|
+
* to send to the backend. Returns Appilot-registry tools today; when only
|
|
32
|
+
* native tools exist (a page that used `navigator.modelContext` directly), the
|
|
33
|
+
* extension's main-world bridge supplies them instead. Kept here so both
|
|
34
|
+
* surfaces share one serialization shape.
|
|
35
|
+
*/
|
|
36
|
+
export declare function collectClientTools(): SerializedClientTool[];
|
|
37
|
+
export { registerClientTool, unregisterClientTool, onClientToolsChange } from './registry.js';
|
|
38
|
+
export type { ClientToolDefinition, ClientToolHandle, SerializedClientTool } from './registry.js';
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `appilot` client actions: the WebMCP-aligned surface a host app calls to
|
|
3
|
+
* expose its actions to the Appilot agent.
|
|
4
|
+
*
|
|
5
|
+
* Two postures, one call site:
|
|
6
|
+
* - POLYFILL (provider): for the 99% of apps with no native WebMCP, our
|
|
7
|
+
* in-page registry IS the tool surface (see registry.ts).
|
|
8
|
+
* - CONSUMER (native): when the browser exposes `navigator.modelContext` /
|
|
9
|
+
* `document.modelContext`, we also delegate registration to it (so the page
|
|
10
|
+
* is a real WebMCP page for any agent), and the collector prefers native
|
|
11
|
+
* tools. Namespace is detect-only because the spec is in flux; we bind to
|
|
12
|
+
* the stable descriptor (name/description/inputSchema/execute).
|
|
13
|
+
*
|
|
14
|
+
* Import from `appilot`. See docs/content-model/tools-client-actions.md.
|
|
15
|
+
*/
|
|
16
|
+
import { registerClientTool, serializeClientTools, } from './registry.js';
|
|
17
|
+
function nativeModelContext() {
|
|
18
|
+
if (typeof navigator !== 'undefined' && navigator.modelContext) {
|
|
19
|
+
return navigator.modelContext;
|
|
20
|
+
}
|
|
21
|
+
if (typeof document !== 'undefined' && document.modelContext) {
|
|
22
|
+
return document.modelContext;
|
|
23
|
+
}
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
/** True when the browser natively implements a WebMCP `modelContext`. */
|
|
27
|
+
export function hasNativeWebMcp() {
|
|
28
|
+
return nativeModelContext() !== undefined;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Register a tool with Appilot, WebMCP-style. The descriptor is the stable
|
|
32
|
+
* WebMCP shape: `{ name, description, inputSchema?, annotations?, execute }`.
|
|
33
|
+
*
|
|
34
|
+
* If the browser exposes a native `modelContext`, we ALSO register there so a
|
|
35
|
+
* native browser agent sees the same tool: "re-emission, not rewrite". We
|
|
36
|
+
* always mirror into Appilot's registry so collection does not depend on the
|
|
37
|
+
* still-unspecified native `getTools()`/`executeTool()` discovery API.
|
|
38
|
+
*/
|
|
39
|
+
export function registerTool(def) {
|
|
40
|
+
const appilotHandle = registerClientTool(def);
|
|
41
|
+
let nativeAbort;
|
|
42
|
+
const native = nativeModelContext();
|
|
43
|
+
if (native?.registerTool) {
|
|
44
|
+
try {
|
|
45
|
+
// Both the spec-draft (returns Promise + AbortController option) and
|
|
46
|
+
// the Chrome-shipped surface accept this descriptor shape.
|
|
47
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : undefined;
|
|
48
|
+
native.registerTool({
|
|
49
|
+
name: def.name,
|
|
50
|
+
description: def.description,
|
|
51
|
+
inputSchema: def.inputSchema,
|
|
52
|
+
annotations: def.annotations,
|
|
53
|
+
execute: def.execute,
|
|
54
|
+
}, controller ? { signal: controller.signal } : undefined);
|
|
55
|
+
nativeAbort = controller;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Native registration is best-effort; Appilot's registry still works.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
unregister() {
|
|
63
|
+
appilotHandle.unregister();
|
|
64
|
+
try {
|
|
65
|
+
nativeAbort?.abort?.();
|
|
66
|
+
}
|
|
67
|
+
catch { /* ignore */ }
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The per-turn collector the extension/widget call to gather the descriptors
|
|
73
|
+
* to send to the backend. Returns Appilot-registry tools today; when only
|
|
74
|
+
* native tools exist (a page that used `navigator.modelContext` directly), the
|
|
75
|
+
* extension's main-world bridge supplies them instead. Kept here so both
|
|
76
|
+
* surfaces share one serialization shape.
|
|
77
|
+
*/
|
|
78
|
+
export function collectClientTools() {
|
|
79
|
+
return serializeClientTools();
|
|
80
|
+
}
|
|
81
|
+
export { registerClientTool, unregisterClientTool, onClientToolsChange } from './registry.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appilot widget boot: load the embeddable assistant into a host app with an
|
|
3
|
+
* identified user token, and keep it authenticated for the life of the page.
|
|
4
|
+
*
|
|
5
|
+
* The host app owns identity: it exposes a backend RELAY endpoint that mints an
|
|
6
|
+
* identified widget token (so the widget key never sees the end user's
|
|
7
|
+
* credentials), and this boot loader POSTs to it, then loads the widget bundle
|
|
8
|
+
* with the returned token.
|
|
9
|
+
*
|
|
10
|
+
* Design guarantees:
|
|
11
|
+
* - Boot does not latch on failure: a failed relay or bundle load returns
|
|
12
|
+
* availability to 'unavailable' and a later bootAppilotWidget() call retries
|
|
13
|
+
* from scratch (one hiccup never permanently disables the assistant).
|
|
14
|
+
* - The widget token (TTL ~3600s) is re-relayed at ~80% of its TTL. The fresh
|
|
15
|
+
* token is written back to the loader script tag (data-user-token) so every
|
|
16
|
+
* subsequent (re)boot stays authenticated past the hour, AND handed to the
|
|
17
|
+
* live instance through `window.Appilot.setUserToken` so a long-lived tab
|
|
18
|
+
* never starts 401ing on a healthy host session.
|
|
19
|
+
* - The widget can request an out-of-band refresh by dispatching
|
|
20
|
+
* 'appilot:identity-refresh-requested' on window (it does so when its
|
|
21
|
+
* federated identity is rejected); the loader re-relays immediately.
|
|
22
|
+
* - When the host supplies `getBearer` and it returns null (the host session
|
|
23
|
+
* is gone), the scheduled refresh does NOT call the relay: relaying without
|
|
24
|
+
* a bearer would mint whatever fallback identity the endpoint assigns,
|
|
25
|
+
* silently swapping the signed-in user for a default persona. The loader
|
|
26
|
+
* retries until the host session is back.
|
|
27
|
+
* - The relay call carries a bearer supplied by the host (`getBearer`) when one
|
|
28
|
+
* exists, so a logged-in user federates as themselves; without it the relay
|
|
29
|
+
* falls back to whatever identity the endpoint assigns.
|
|
30
|
+
* - Availability is observable (getWidgetAvailability / subscribeWidgetAvailability)
|
|
31
|
+
* so pages can render an honest "assistant unavailable" state. Pairs with
|
|
32
|
+
* React useSyncExternalStore(subscribeWidgetAvailability, getWidgetAvailability).
|
|
33
|
+
*/
|
|
34
|
+
export type WidgetAvailability = 'unknown' | 'booting' | 'ready' | 'unavailable';
|
|
35
|
+
export interface AppilotWidgetBootOptions {
|
|
36
|
+
/** Widget bundle URL. Missing = no assistant. */
|
|
37
|
+
widgetScriptUrl: string | undefined;
|
|
38
|
+
/** Publishable widget key. */
|
|
39
|
+
widgetKey?: string;
|
|
40
|
+
/** Appilot backend base URL. */
|
|
41
|
+
appilotApiUrl?: string;
|
|
42
|
+
/** Identity relay endpoint on the host app's own backend. */
|
|
43
|
+
tokenEndpoint?: string;
|
|
44
|
+
/** JSON body for the relay, e.g. { persona: 'learner' }. Omitted = relay default. */
|
|
45
|
+
tokenBody?: Record<string, unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* Supplies the host session bearer attached to the relay call so the relay
|
|
48
|
+
* can federate the LOGGED-IN user instead of a default identity. Return null
|
|
49
|
+
* when no user session exists. Host-specific; keeps this loader app-agnostic.
|
|
50
|
+
*/
|
|
51
|
+
getBearer?: () => string | null | undefined;
|
|
52
|
+
/** How the app names its assistant in console diagnostics ('assistant', 'Appilot'). */
|
|
53
|
+
surfaceLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Header brand text and assistant label inside the panel (`data-brand-name`).
|
|
56
|
+
* Omitted, the widget falls back to the organization name from `/widget/init`,
|
|
57
|
+
* which white-labels the panel with the HOST's brand. An app that wants the
|
|
58
|
+
* assistant to carry its own name passes it here.
|
|
59
|
+
*/
|
|
60
|
+
brandName?: string;
|
|
61
|
+
/** Maximum time for token relay + bundle readiness. Defaults to 10 seconds. */
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
position?: 'bottom-right' | 'bottom-left';
|
|
64
|
+
theme?: string;
|
|
65
|
+
language?: string;
|
|
66
|
+
}
|
|
67
|
+
export declare function getWidgetAvailability(): WidgetAvailability;
|
|
68
|
+
/** Subscribe to availability changes. Returns the unsubscribe function. */
|
|
69
|
+
export declare function subscribeWidgetAvailability(listener: (state: WidgetAvailability) => void): () => void;
|
|
70
|
+
export declare function bootAppilotWidget(options: AppilotWidgetBootOptions): Promise<void>;
|
|
71
|
+
/** Test-only: reset module state between tests. Never call from app code. */
|
|
72
|
+
export declare function __resetAppilotWidgetForTests(): void;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Appilot widget boot: load the embeddable assistant into a host app with an
|
|
3
|
+
* identified user token, and keep it authenticated for the life of the page.
|
|
4
|
+
*
|
|
5
|
+
* The host app owns identity: it exposes a backend RELAY endpoint that mints an
|
|
6
|
+
* identified widget token (so the widget key never sees the end user's
|
|
7
|
+
* credentials), and this boot loader POSTs to it, then loads the widget bundle
|
|
8
|
+
* with the returned token.
|
|
9
|
+
*
|
|
10
|
+
* Design guarantees:
|
|
11
|
+
* - Boot does not latch on failure: a failed relay or bundle load returns
|
|
12
|
+
* availability to 'unavailable' and a later bootAppilotWidget() call retries
|
|
13
|
+
* from scratch (one hiccup never permanently disables the assistant).
|
|
14
|
+
* - The widget token (TTL ~3600s) is re-relayed at ~80% of its TTL. The fresh
|
|
15
|
+
* token is written back to the loader script tag (data-user-token) so every
|
|
16
|
+
* subsequent (re)boot stays authenticated past the hour, AND handed to the
|
|
17
|
+
* live instance through `window.Appilot.setUserToken` so a long-lived tab
|
|
18
|
+
* never starts 401ing on a healthy host session.
|
|
19
|
+
* - The widget can request an out-of-band refresh by dispatching
|
|
20
|
+
* 'appilot:identity-refresh-requested' on window (it does so when its
|
|
21
|
+
* federated identity is rejected); the loader re-relays immediately.
|
|
22
|
+
* - When the host supplies `getBearer` and it returns null (the host session
|
|
23
|
+
* is gone), the scheduled refresh does NOT call the relay: relaying without
|
|
24
|
+
* a bearer would mint whatever fallback identity the endpoint assigns,
|
|
25
|
+
* silently swapping the signed-in user for a default persona. The loader
|
|
26
|
+
* retries until the host session is back.
|
|
27
|
+
* - The relay call carries a bearer supplied by the host (`getBearer`) when one
|
|
28
|
+
* exists, so a logged-in user federates as themselves; without it the relay
|
|
29
|
+
* falls back to whatever identity the endpoint assigns.
|
|
30
|
+
* - Availability is observable (getWidgetAvailability / subscribeWidgetAvailability)
|
|
31
|
+
* so pages can render an honest "assistant unavailable" state. Pairs with
|
|
32
|
+
* React useSyncExternalStore(subscribeWidgetAvailability, getWidgetAvailability).
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_APPILOT_API_URL = 'http://localhost:6001';
|
|
35
|
+
/** Dispatched by the widget when its federated identity is rejected (401),
|
|
36
|
+
* asking the loader for an immediate re-relay instead of waiting for the
|
|
37
|
+
* scheduled ~80%-TTL refresh. Mirror of the widget's WidgetSDK constant. */
|
|
38
|
+
const IDENTITY_REFRESH_REQUESTED_EVENT = 'appilot:identity-refresh-requested';
|
|
39
|
+
const DEFAULT_TOKEN_ENDPOINT = '/api/widget/token';
|
|
40
|
+
const DEFAULT_TOKEN_TTL_SECONDS = 3600;
|
|
41
|
+
const TOKEN_REFRESH_FRACTION = 0.8;
|
|
42
|
+
const REFRESH_RETRY_MS = 60_000;
|
|
43
|
+
const DEFAULT_BOOT_TIMEOUT_MS = 10_000;
|
|
44
|
+
let availability = 'unknown';
|
|
45
|
+
const listeners = new Set();
|
|
46
|
+
let scriptEl = null;
|
|
47
|
+
let refreshTimer = null;
|
|
48
|
+
let bootTimer = null;
|
|
49
|
+
let bootAbortController = null;
|
|
50
|
+
let activeAttempt = 0;
|
|
51
|
+
let warnedMissingConfig = false;
|
|
52
|
+
let refreshRequestListener = null;
|
|
53
|
+
function detachRefreshRequestListener() {
|
|
54
|
+
if (refreshRequestListener) {
|
|
55
|
+
window.removeEventListener(IDENTITY_REFRESH_REQUESTED_EVENT, refreshRequestListener);
|
|
56
|
+
refreshRequestListener = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function attachRefreshRequestListener(options) {
|
|
60
|
+
detachRefreshRequestListener();
|
|
61
|
+
refreshRequestListener = () => {
|
|
62
|
+
clearRefreshTimer();
|
|
63
|
+
void refreshWidgetToken(options);
|
|
64
|
+
};
|
|
65
|
+
window.addEventListener(IDENTITY_REFRESH_REQUESTED_EVENT, refreshRequestListener);
|
|
66
|
+
}
|
|
67
|
+
function setAvailability(next) {
|
|
68
|
+
if (availability === next)
|
|
69
|
+
return;
|
|
70
|
+
availability = next;
|
|
71
|
+
for (const listener of listeners)
|
|
72
|
+
listener(next);
|
|
73
|
+
}
|
|
74
|
+
export function getWidgetAvailability() {
|
|
75
|
+
return availability;
|
|
76
|
+
}
|
|
77
|
+
/** Subscribe to availability changes. Returns the unsubscribe function. */
|
|
78
|
+
export function subscribeWidgetAvailability(listener) {
|
|
79
|
+
listeners.add(listener);
|
|
80
|
+
return () => {
|
|
81
|
+
listeners.delete(listener);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function relayWidgetToken(options, signal) {
|
|
85
|
+
const endpoint = options.tokenEndpoint ?? DEFAULT_TOKEN_ENDPOINT;
|
|
86
|
+
// The host bearer lets the relay federate the LOGGED-IN user instead of a
|
|
87
|
+
// default identity; without it the relay assigns whatever identity it wants.
|
|
88
|
+
const bearer = options.getBearer?.() ?? null;
|
|
89
|
+
const response = await fetch(endpoint, {
|
|
90
|
+
method: 'POST',
|
|
91
|
+
signal,
|
|
92
|
+
headers: {
|
|
93
|
+
...(bearer ? { Authorization: `Bearer ${bearer}` } : {}),
|
|
94
|
+
...(options.tokenBody !== undefined ? { 'Content-Type': 'application/json' } : {}),
|
|
95
|
+
},
|
|
96
|
+
...(options.tokenBody !== undefined ? { body: JSON.stringify(options.tokenBody) } : {}),
|
|
97
|
+
});
|
|
98
|
+
if (!response.ok) {
|
|
99
|
+
throw new Error(`widget token relay responded ${response.status}`);
|
|
100
|
+
}
|
|
101
|
+
return (await response.json());
|
|
102
|
+
}
|
|
103
|
+
function clearRefreshTimer() {
|
|
104
|
+
if (refreshTimer !== null) {
|
|
105
|
+
clearTimeout(refreshTimer);
|
|
106
|
+
refreshTimer = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function clearBootTimer() {
|
|
110
|
+
if (bootTimer !== null) {
|
|
111
|
+
clearTimeout(bootTimer);
|
|
112
|
+
bootTimer = null;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function hasRequiredWidgetApi() {
|
|
116
|
+
return typeof window.Appilot?.open === 'function';
|
|
117
|
+
}
|
|
118
|
+
function cleanFailedAttempt(attempt, script) {
|
|
119
|
+
if (attempt !== activeAttempt) {
|
|
120
|
+
script?.remove();
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
activeAttempt += 1;
|
|
124
|
+
clearBootTimer();
|
|
125
|
+
clearRefreshTimer();
|
|
126
|
+
detachRefreshRequestListener();
|
|
127
|
+
bootAbortController?.abort();
|
|
128
|
+
bootAbortController = null;
|
|
129
|
+
const target = script ?? scriptEl;
|
|
130
|
+
target?.remove();
|
|
131
|
+
if (!target || scriptEl === target)
|
|
132
|
+
scriptEl = null;
|
|
133
|
+
setAvailability('unavailable');
|
|
134
|
+
}
|
|
135
|
+
function scheduleTokenRefresh(expiresIn, options) {
|
|
136
|
+
clearRefreshTimer();
|
|
137
|
+
const ttl = expiresIn && expiresIn > 0 ? expiresIn : DEFAULT_TOKEN_TTL_SECONDS;
|
|
138
|
+
refreshTimer = setTimeout(() => {
|
|
139
|
+
void refreshWidgetToken(options);
|
|
140
|
+
}, ttl * TOKEN_REFRESH_FRACTION * 1000);
|
|
141
|
+
}
|
|
142
|
+
async function refreshWidgetToken(options) {
|
|
143
|
+
// Host session gone: do NOT hit the relay bearerless. The relay would fall
|
|
144
|
+
// back to its default identity (dev personas in development), silently
|
|
145
|
+
// swapping the federated user on the next token. Wait for the session to
|
|
146
|
+
// come back (login) and retry; the widget shows its own identity-expired
|
|
147
|
+
// state in the meantime.
|
|
148
|
+
if (options.getBearer && !options.getBearer()) {
|
|
149
|
+
clearRefreshTimer();
|
|
150
|
+
refreshTimer = setTimeout(() => {
|
|
151
|
+
void refreshWidgetToken(options);
|
|
152
|
+
}, REFRESH_RETRY_MS);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const relayed = await relayWidgetToken(options);
|
|
157
|
+
// Keep the loader tag fresh so any widget (re)boot past the original TTL
|
|
158
|
+
// stays authenticated.
|
|
159
|
+
scriptEl?.setAttribute('data-user-token', relayed.token);
|
|
160
|
+
// Feed the live instance so an open tab adopts the fresh token without
|
|
161
|
+
// a reload (`window.Appilot.setUserToken`, widget >= 2026-08-25).
|
|
162
|
+
const appilot = window.Appilot;
|
|
163
|
+
appilot?.setUserToken?.(relayed.token);
|
|
164
|
+
scheduleTokenRefresh(relayed.expiresIn, options);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
console.warn('[appilot] widget token refresh failed; retrying shortly', error);
|
|
168
|
+
clearRefreshTimer();
|
|
169
|
+
refreshTimer = setTimeout(() => {
|
|
170
|
+
void refreshWidgetToken(options);
|
|
171
|
+
}, REFRESH_RETRY_MS);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
export async function bootAppilotWidget(options) {
|
|
175
|
+
// Idempotent while healthy; 'unavailable' deliberately falls through so a
|
|
176
|
+
// later call retries after a failure.
|
|
177
|
+
if (availability === 'booting' || availability === 'ready')
|
|
178
|
+
return;
|
|
179
|
+
const label = options.surfaceLabel ?? 'assistant';
|
|
180
|
+
if (!options.widgetScriptUrl) {
|
|
181
|
+
if (!warnedMissingConfig) {
|
|
182
|
+
warnedMissingConfig = true;
|
|
183
|
+
console.warn(`[appilot] widget script URL is not configured; ${label} disabled`);
|
|
184
|
+
}
|
|
185
|
+
setAvailability('unavailable');
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
setAvailability('booting');
|
|
189
|
+
const attempt = ++activeAttempt;
|
|
190
|
+
bootAbortController?.abort();
|
|
191
|
+
bootAbortController = new AbortController();
|
|
192
|
+
clearBootTimer();
|
|
193
|
+
bootTimer = setTimeout(() => {
|
|
194
|
+
if (attempt !== activeAttempt)
|
|
195
|
+
return;
|
|
196
|
+
console.warn(`[appilot] widget boot timed out; ${label} disabled`);
|
|
197
|
+
cleanFailedAttempt(attempt);
|
|
198
|
+
}, options.timeoutMs ?? DEFAULT_BOOT_TIMEOUT_MS);
|
|
199
|
+
let relayed;
|
|
200
|
+
try {
|
|
201
|
+
relayed = await relayWidgetToken(options, bootAbortController.signal);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (attempt !== activeAttempt)
|
|
205
|
+
return;
|
|
206
|
+
console.warn(`[appilot] widget token relay unavailable; ${label} disabled`, error);
|
|
207
|
+
cleanFailedAttempt(attempt);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (attempt !== activeAttempt)
|
|
211
|
+
return;
|
|
212
|
+
const script = document.createElement('script');
|
|
213
|
+
script.src = options.widgetScriptUrl;
|
|
214
|
+
script.async = true;
|
|
215
|
+
if (options.widgetKey)
|
|
216
|
+
script.dataset.apiKey = options.widgetKey;
|
|
217
|
+
script.dataset.apiUrl = options.appilotApiUrl || DEFAULT_APPILOT_API_URL;
|
|
218
|
+
script.dataset.userToken = relayed.token;
|
|
219
|
+
if (options.brandName)
|
|
220
|
+
script.dataset.brandName = options.brandName;
|
|
221
|
+
script.dataset.position = options.position ?? 'bottom-right';
|
|
222
|
+
script.dataset.theme = options.theme ?? 'host';
|
|
223
|
+
script.dataset.language = options.language ?? 'en';
|
|
224
|
+
script.addEventListener('load', () => {
|
|
225
|
+
if (attempt !== activeAttempt) {
|
|
226
|
+
script.remove();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (!hasRequiredWidgetApi()) {
|
|
230
|
+
console.warn(`[appilot] widget bundle loaded without its panel API; ${label} disabled`);
|
|
231
|
+
cleanFailedAttempt(attempt, script);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
clearBootTimer();
|
|
235
|
+
bootAbortController = null;
|
|
236
|
+
setAvailability('ready');
|
|
237
|
+
scheduleTokenRefresh(relayed.expiresIn, options);
|
|
238
|
+
attachRefreshRequestListener(options);
|
|
239
|
+
});
|
|
240
|
+
script.addEventListener('error', () => {
|
|
241
|
+
if (attempt !== activeAttempt) {
|
|
242
|
+
script.remove();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
console.warn(`[appilot] widget bundle failed to load; ${label} disabled`);
|
|
246
|
+
cleanFailedAttempt(attempt, script);
|
|
247
|
+
});
|
|
248
|
+
document.head.appendChild(script);
|
|
249
|
+
scriptEl = script;
|
|
250
|
+
}
|
|
251
|
+
/** Test-only: reset module state between tests. Never call from app code. */
|
|
252
|
+
export function __resetAppilotWidgetForTests() {
|
|
253
|
+
activeAttempt += 1;
|
|
254
|
+
clearBootTimer();
|
|
255
|
+
bootAbortController?.abort();
|
|
256
|
+
bootAbortController = null;
|
|
257
|
+
clearRefreshTimer();
|
|
258
|
+
detachRefreshRequestListener();
|
|
259
|
+
scriptEl?.remove();
|
|
260
|
+
scriptEl = null;
|
|
261
|
+
listeners.clear();
|
|
262
|
+
availability = 'unknown';
|
|
263
|
+
warnedMissingConfig = false;
|
|
264
|
+
}
|
package/package.json
CHANGED
|
@@ -1,17 +1,52 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
2
|
+
"name": "appilot",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Appilot SDK: the single developer-facing surface for building on Appilot. Boot the assistant (bootAppilotWidget), register client actions (registerTool, WebMCP-aligned), and run Focused Sessions (startFocusedSession). Browser-only, React-free, zero runtime dependencies.",
|
|
5
|
+
"homepage": "https://appilot.space",
|
|
6
|
+
"author": "BetterKnow GmbH",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"appilot",
|
|
9
|
+
"webmcp",
|
|
10
|
+
"agent",
|
|
11
|
+
"assistant",
|
|
12
|
+
"widget",
|
|
13
|
+
"sdk"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"main": "./dist/index.js",
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"default": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./runtime": {
|
|
24
|
+
"types": "./dist/runtime.d.ts",
|
|
25
|
+
"default": "./dist/runtime.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"sideEffects": false,
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"jsdom": "^26.1.0",
|
|
34
|
+
"typescript": "^5.5.0",
|
|
35
|
+
"vitest": "^2.0.0"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
|
+
},
|
|
44
|
+
"license": "ISC",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc",
|
|
47
|
+
"typecheck": "tsc --noEmit",
|
|
48
|
+
"type-check": "tsc --noEmit",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest"
|
|
51
|
+
}
|
|
52
|
+
}
|