chatablex-web-sdk 1.0.3 → 1.0.31
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 -0
- package/README.zh-CN.md +53 -0
- package/dist/index.d.mts +37 -1
- package/dist/index.d.ts +37 -1
- package/dist/index.js +61 -2
- package/dist/index.mjs +61 -2
- package/package.json +1 -1
- package/src/index.ts +2 -0
- package/src/modules/auth.ts +102 -0
- package/src/types.ts +42 -0
package/README.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# ChatableX Web SDK
|
|
2
2
|
|
|
3
|
+
[](https://github.com/chatablex/chatablex-web-sdk/blob/main/package.json)
|
|
4
|
+
[](https://www.npmjs.com/package/chatablex-web-sdk)
|
|
5
|
+
[](https://github.com/chatablex/chatablex-web-sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/chatablex/chatablex-web-sdk/pulls)
|
|
8
|
+
|
|
3
9
|
English | [**简体中文**](README.zh-CN.md)
|
|
4
10
|
|
|
5
11
|
**Runtime SDK for building ChatableX AI App WebUI extensions.**
|
|
@@ -27,6 +33,7 @@ Your WebUI runs inside a WebView. Many capabilities — native dialogs, file pic
|
|
|
27
33
|
- [sdk.storage](#sdkstorage)
|
|
28
34
|
- [sdk.tools](#sdktools)
|
|
29
35
|
- [sdk.platform](#sdkplatform)
|
|
36
|
+
- [sdk.auth](#sdkauth)
|
|
30
37
|
- [Events Reference](#events-reference)
|
|
31
38
|
- [Permissions](#permissions)
|
|
32
39
|
- [Host Capability Matrix](#host-capability-matrix)
|
|
@@ -533,6 +540,52 @@ Throws if `targetUrl` is empty or whitespace-only.
|
|
|
533
540
|
|
|
534
541
|
---
|
|
535
542
|
|
|
543
|
+
### `sdk.auth`
|
|
544
|
+
|
|
545
|
+
Unified authentication for **every** WebUI app. In a hosted (Flutter WebView)
|
|
546
|
+
environment it transparently reuses the desktop login session — your app writes
|
|
547
|
+
**zero** login/token code. Just call `getAuthHeaders()` and attach it to your
|
|
548
|
+
`fetch`.
|
|
549
|
+
|
|
550
|
+
| Method | Signature | Description |
|
|
551
|
+
|--------|-----------|-------------|
|
|
552
|
+
| `getToken` | `() => Promise<AuthTokenData \| null>` | Get a valid token (cached, refreshed on demand). `null` when not authenticated. |
|
|
553
|
+
| `getAuthHeaders` | `() => Promise<Record<string,string>>` | `{ Authorization: "Bearer <token>" }`, or `{}` when not authenticated. |
|
|
554
|
+
| `getUserId` | `() => string \| null` | Authenticated user id (sync, cache only). |
|
|
555
|
+
| `isAuthenticated` | `() => boolean` | Whether a valid token is cached (sync). |
|
|
556
|
+
| `refresh` | `() => Promise<boolean>` | Force a refresh via the host. Concurrent calls are merged (single-flight). |
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
// Attach the host login session to any authenticated request — no login code.
|
|
560
|
+
const res = await fetch('https://api.example.com/scenes', {
|
|
561
|
+
headers: {
|
|
562
|
+
'Content-Type': 'application/json',
|
|
563
|
+
...(await sdk.auth.getAuthHeaders()),
|
|
564
|
+
},
|
|
565
|
+
});
|
|
566
|
+
|
|
567
|
+
if (!sdk.auth.isAuthenticated()) {
|
|
568
|
+
// not logged in / not hosted — disable features that need auth
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// On a 401 from your backend, force a refresh and retry once:
|
|
572
|
+
if (res.status === 401 && (await sdk.auth.refresh())) {
|
|
573
|
+
// retry with await sdk.auth.getAuthHeaders()
|
|
574
|
+
}
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Behavior & guarantees**
|
|
578
|
+
|
|
579
|
+
- **Token is in-memory only.** The `refresh_token` never crosses the bridge.
|
|
580
|
+
- **Pre-emptive refresh.** The host refreshes the `access_token` before sending
|
|
581
|
+
when it is expired or near expiry, so you normally never see an expired token.
|
|
582
|
+
- **Safe degradation.** Outside a WebView, or when the host is logged out,
|
|
583
|
+
`getAuthHeaders()` resolves to `{}` and never throws.
|
|
584
|
+
- **Pluggable provider.** Today only `HostAuthProvider` (WebView) is wired up; a
|
|
585
|
+
future browser/unified-login provider will slot in without changing your code.
|
|
586
|
+
|
|
587
|
+
---
|
|
588
|
+
|
|
536
589
|
## Events Reference
|
|
537
590
|
|
|
538
591
|
| Event | Payload | When fired |
|
|
@@ -576,6 +629,7 @@ SDK methods are thin RPC wrappers. Some host handlers are fully implemented; oth
|
|
|
576
629
|
| `sdk.ui.pickFile` | **Production** | Requires `file_access` |
|
|
577
630
|
| `sdk.ui.updateState` | **Production** | Delegates to host |
|
|
578
631
|
| `sdk.platform.openInBrowser` | **Production** | Auth handoff |
|
|
632
|
+
| `sdk.auth.*` | **Production** | Hosted: reuses desktop login via `host.getAuthToken` (pre-refresh) |
|
|
579
633
|
| `sdk.ai.chat` | **Production** | Requires `ai_chat` + delegate |
|
|
580
634
|
| `sdk.ai.getContext` | **Partial** | Returns minimal context |
|
|
581
635
|
| `sdk.ai.chatStream` | **Partial** | Returns `{ streaming: true }`; tokens via events |
|
package/README.zh-CN.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# ChatableX Web SDK
|
|
2
2
|
|
|
3
|
+
[](https://github.com/chatablex/chatablex-web-sdk/blob/main/package.json)
|
|
4
|
+
[](https://www.npmjs.com/package/chatablex-web-sdk)
|
|
5
|
+
[](https://github.com/chatablex/chatablex-web-sdk/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://github.com/chatablex/chatablex-web-sdk/pulls)
|
|
8
|
+
|
|
3
9
|
**[English](README.md)** | 简体中文
|
|
4
10
|
|
|
5
11
|
**用于构建 ChatableX AI App WebUI 扩展的官方运行时 SDK。**
|
|
@@ -27,6 +33,7 @@
|
|
|
27
33
|
- [sdk.storage](#sdkstorage)
|
|
28
34
|
- [sdk.tools](#sdktools)
|
|
29
35
|
- [sdk.platform](#sdkplatform)
|
|
36
|
+
- [sdk.auth](#sdkauth)
|
|
30
37
|
- [事件参考](#事件参考)
|
|
31
38
|
- [权限声明](#权限声明)
|
|
32
39
|
- [宿主能力矩阵](#宿主能力矩阵)
|
|
@@ -533,6 +540,51 @@ await sdk.platform.openInBrowser('https://docs.example.com/guide');
|
|
|
533
540
|
|
|
534
541
|
---
|
|
535
542
|
|
|
543
|
+
### `sdk.auth`
|
|
544
|
+
|
|
545
|
+
面向**所有** WebUI 应用的统一鉴权入口。在宿主(Flutter WebView)环境下,它会
|
|
546
|
+
透明复用桌面端的登录态——你的应用**无需编写任何登录/Token 代码**,只需调用
|
|
547
|
+
`getAuthHeaders()` 并附加到 `fetch` 即可。
|
|
548
|
+
|
|
549
|
+
| 方法 | 签名 | 说明 |
|
|
550
|
+
|------|------|------|
|
|
551
|
+
| `getToken` | `() => Promise<AuthTokenData \| null>` | 取有效 Token(内存缓存,按需刷新);未登录返回 `null`。 |
|
|
552
|
+
| `getAuthHeaders` | `() => Promise<Record<string,string>>` | 返回 `{ Authorization: "Bearer <token>" }`,未登录则返回 `{}`。 |
|
|
553
|
+
| `getUserId` | `() => string \| null` | 当前登录用户 id(同步,仅读缓存)。 |
|
|
554
|
+
| `isAuthenticated` | `() => boolean` | 是否已缓存有效 Token(同步)。 |
|
|
555
|
+
| `refresh` | `() => Promise<boolean>` | 强制经宿主刷新;并发调用合并为一次(single-flight)。 |
|
|
556
|
+
|
|
557
|
+
```ts
|
|
558
|
+
// 给任意需要鉴权的请求附加宿主登录态——无需任何登录代码。
|
|
559
|
+
const res = await fetch('https://api.example.com/scenes', {
|
|
560
|
+
headers: {
|
|
561
|
+
'Content-Type': 'application/json',
|
|
562
|
+
...(await sdk.auth.getAuthHeaders()),
|
|
563
|
+
},
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
if (!sdk.auth.isAuthenticated()) {
|
|
567
|
+
// 未登录 / 非 WebView——禁用需要鉴权的功能
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// 后端返回 401 时,强制刷新并重试一次:
|
|
571
|
+
if (res.status === 401 && (await sdk.auth.refresh())) {
|
|
572
|
+
// 用 await sdk.auth.getAuthHeaders() 重试
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
**行为与保证**
|
|
577
|
+
|
|
578
|
+
- **Token 仅存内存。** `refresh_token` 永不经过 bridge。
|
|
579
|
+
- **下发前刷新。** 宿主在 Token 过期或临近过期时会先刷新再下发,正常情况下你
|
|
580
|
+
不会拿到过期 Token。
|
|
581
|
+
- **安全降级。** 在非 WebView 或宿主未登录时,`getAuthHeaders()` 返回 `{}` 且
|
|
582
|
+
不抛异常。
|
|
583
|
+
- **Provider 可插拔。** 目前仅接入 `HostAuthProvider`(WebView);未来的浏览器/
|
|
584
|
+
统一登录 provider 可无缝替换,消费方代码无需改动。
|
|
585
|
+
|
|
586
|
+
---
|
|
587
|
+
|
|
536
588
|
## 事件参考
|
|
537
589
|
|
|
538
590
|
| 事件 | 载荷 | 触发时机 |
|
|
@@ -576,6 +628,7 @@ SDK 方法是薄 RPC 封装。部分宿主处理器已完整实现,部分返
|
|
|
576
628
|
| `sdk.ui.pickFile` | **生产可用** | 需要 `file_access` |
|
|
577
629
|
| `sdk.ui.updateState` | **生产可用** | 委托给宿主 |
|
|
578
630
|
| `sdk.platform.openInBrowser` | **生产可用** | 鉴权传递 |
|
|
631
|
+
| `sdk.auth.*` | **生产可用** | 宿主态:经 `host.getAuthToken` 复用桌面登录(下发前刷新) |
|
|
579
632
|
| `sdk.ai.chat` | **生产可用** | 需要 `ai_chat` + delegate |
|
|
580
633
|
| `sdk.ai.getContext` | **部分实现** | 返回最小上下文 |
|
|
581
634
|
| `sdk.ai.chatStream` | **部分实现** | 返回 `{ streaming: true }`;token 走事件 |
|
package/dist/index.d.mts
CHANGED
|
@@ -156,6 +156,41 @@ interface ChatableXPlatform {
|
|
|
156
156
|
/** Open URL in system browser with auth handoff (WebView only; implemented by Flutter host). */
|
|
157
157
|
openInBrowser(targetUrl: string): Promise<void>;
|
|
158
158
|
}
|
|
159
|
+
/** Token payload returned by the host (never includes the refresh_token). */
|
|
160
|
+
interface AuthTokenData {
|
|
161
|
+
/** Bearer access token to put in the Authorization header. */
|
|
162
|
+
access_token: string;
|
|
163
|
+
/** Access token expiry, epoch milliseconds. */
|
|
164
|
+
expires_at: number;
|
|
165
|
+
/** Authenticated user id. */
|
|
166
|
+
user_id: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Unified auth entry point for all WebUI apps.
|
|
170
|
+
*
|
|
171
|
+
* In a hosted (Flutter WebView) environment this reuses the desktop login
|
|
172
|
+
* session via the `host.getAuthToken` bridge call — apps never implement
|
|
173
|
+
* login or token handling themselves.
|
|
174
|
+
*/
|
|
175
|
+
interface ChatableXAuth {
|
|
176
|
+
/**
|
|
177
|
+
* Get a valid access token. Returns the in-memory cached token when still
|
|
178
|
+
* valid, otherwise fetches a fresh one from the host. Returns `null` when
|
|
179
|
+
* not authenticated / not hosted.
|
|
180
|
+
*/
|
|
181
|
+
getToken(): Promise<AuthTokenData | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Build auth headers ready to spread into a `fetch`. Returns
|
|
184
|
+
* `{ Authorization: "Bearer <token>" }` when authenticated, otherwise `{}`.
|
|
185
|
+
*/
|
|
186
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
187
|
+
/** Currently authenticated user id, or `null`. Synchronous (cache only). */
|
|
188
|
+
getUserId(): string | null;
|
|
189
|
+
/** Whether a valid token is currently cached. Synchronous (cache only). */
|
|
190
|
+
isAuthenticated(): boolean;
|
|
191
|
+
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
192
|
+
refresh(): Promise<boolean>;
|
|
193
|
+
}
|
|
159
194
|
interface ChatableXSDK {
|
|
160
195
|
ai: ChatableXAI;
|
|
161
196
|
tools: ChatableXTools;
|
|
@@ -164,6 +199,7 @@ interface ChatableXSDK {
|
|
|
164
199
|
storage: ChatableXStorage;
|
|
165
200
|
tool: ChatableXToolModule;
|
|
166
201
|
platform: ChatableXPlatform;
|
|
202
|
+
auth: ChatableXAuth;
|
|
167
203
|
}
|
|
168
204
|
declare global {
|
|
169
205
|
interface Window {
|
|
@@ -252,4 +288,4 @@ declare const ChatableX: {
|
|
|
252
288
|
version: string;
|
|
253
289
|
};
|
|
254
290
|
|
|
255
|
-
export { type AiResponseEventData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
|
291
|
+
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
package/dist/index.d.ts
CHANGED
|
@@ -156,6 +156,41 @@ interface ChatableXPlatform {
|
|
|
156
156
|
/** Open URL in system browser with auth handoff (WebView only; implemented by Flutter host). */
|
|
157
157
|
openInBrowser(targetUrl: string): Promise<void>;
|
|
158
158
|
}
|
|
159
|
+
/** Token payload returned by the host (never includes the refresh_token). */
|
|
160
|
+
interface AuthTokenData {
|
|
161
|
+
/** Bearer access token to put in the Authorization header. */
|
|
162
|
+
access_token: string;
|
|
163
|
+
/** Access token expiry, epoch milliseconds. */
|
|
164
|
+
expires_at: number;
|
|
165
|
+
/** Authenticated user id. */
|
|
166
|
+
user_id: string;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Unified auth entry point for all WebUI apps.
|
|
170
|
+
*
|
|
171
|
+
* In a hosted (Flutter WebView) environment this reuses the desktop login
|
|
172
|
+
* session via the `host.getAuthToken` bridge call — apps never implement
|
|
173
|
+
* login or token handling themselves.
|
|
174
|
+
*/
|
|
175
|
+
interface ChatableXAuth {
|
|
176
|
+
/**
|
|
177
|
+
* Get a valid access token. Returns the in-memory cached token when still
|
|
178
|
+
* valid, otherwise fetches a fresh one from the host. Returns `null` when
|
|
179
|
+
* not authenticated / not hosted.
|
|
180
|
+
*/
|
|
181
|
+
getToken(): Promise<AuthTokenData | null>;
|
|
182
|
+
/**
|
|
183
|
+
* Build auth headers ready to spread into a `fetch`. Returns
|
|
184
|
+
* `{ Authorization: "Bearer <token>" }` when authenticated, otherwise `{}`.
|
|
185
|
+
*/
|
|
186
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
187
|
+
/** Currently authenticated user id, or `null`. Synchronous (cache only). */
|
|
188
|
+
getUserId(): string | null;
|
|
189
|
+
/** Whether a valid token is currently cached. Synchronous (cache only). */
|
|
190
|
+
isAuthenticated(): boolean;
|
|
191
|
+
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
192
|
+
refresh(): Promise<boolean>;
|
|
193
|
+
}
|
|
159
194
|
interface ChatableXSDK {
|
|
160
195
|
ai: ChatableXAI;
|
|
161
196
|
tools: ChatableXTools;
|
|
@@ -164,6 +199,7 @@ interface ChatableXSDK {
|
|
|
164
199
|
storage: ChatableXStorage;
|
|
165
200
|
tool: ChatableXToolModule;
|
|
166
201
|
platform: ChatableXPlatform;
|
|
202
|
+
auth: ChatableXAuth;
|
|
167
203
|
}
|
|
168
204
|
declare global {
|
|
169
205
|
interface Window {
|
|
@@ -252,4 +288,4 @@ declare const ChatableX: {
|
|
|
252
288
|
version: string;
|
|
253
289
|
};
|
|
254
290
|
|
|
255
|
-
export { type AiResponseEventData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
|
291
|
+
export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, type EventCallbackMap, type EventType, type FilePickerOptions, type Message, type NotificationType, SDK_VERSION, type SessionContext, type StateUpdate, type StreamingContentEventData, type TabConfig, type ToolCall, type ToolExecuteHandler, type ToolExecutionEventData, type ToolInfo, type ToolParameter, type ToolResult, type Unsubscribe, type UserMessageEventData };
|
package/dist/index.js
CHANGED
|
@@ -299,10 +299,68 @@ function createPlatformModule(bridge) {
|
|
|
299
299
|
};
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
+
// src/modules/auth.ts
|
|
303
|
+
var EXPIRY_SKEW_MS = 5e3;
|
|
304
|
+
function isValid(token, now) {
|
|
305
|
+
return !!token && typeof token.access_token === "string" && token.access_token.length > 0 && token.expires_at - EXPIRY_SKEW_MS > now;
|
|
306
|
+
}
|
|
307
|
+
var HostAuthProvider = class {
|
|
308
|
+
constructor(bridge) {
|
|
309
|
+
this._token = null;
|
|
310
|
+
/** In-flight refresh, so concurrent callers share one host round-trip. */
|
|
311
|
+
this._refreshing = null;
|
|
312
|
+
this._bridge = bridge;
|
|
313
|
+
}
|
|
314
|
+
async getToken() {
|
|
315
|
+
if (isValid(this._token, Date.now())) return this._token;
|
|
316
|
+
const ok = await this.refresh();
|
|
317
|
+
return ok ? this._token : null;
|
|
318
|
+
}
|
|
319
|
+
async getAuthHeaders() {
|
|
320
|
+
const token = await this.getToken();
|
|
321
|
+
if (!token) return {};
|
|
322
|
+
return { Authorization: `Bearer ${token.access_token}` };
|
|
323
|
+
}
|
|
324
|
+
getUserId() {
|
|
325
|
+
return this._token?.user_id ?? null;
|
|
326
|
+
}
|
|
327
|
+
isAuthenticated() {
|
|
328
|
+
return isValid(this._token, Date.now());
|
|
329
|
+
}
|
|
330
|
+
refresh() {
|
|
331
|
+
if (this._refreshing) return this._refreshing;
|
|
332
|
+
this._refreshing = this._doRefresh().finally(() => {
|
|
333
|
+
this._refreshing = null;
|
|
334
|
+
});
|
|
335
|
+
return this._refreshing;
|
|
336
|
+
}
|
|
337
|
+
async _doRefresh() {
|
|
338
|
+
try {
|
|
339
|
+
const raw = await this._bridge.sendMessage("host.getAuthToken");
|
|
340
|
+
if (raw && typeof raw === "object" && typeof raw.access_token === "string" && raw.access_token.length > 0) {
|
|
341
|
+
this._token = {
|
|
342
|
+
access_token: raw.access_token,
|
|
343
|
+
expires_at: Number(raw.expires_at) || 0,
|
|
344
|
+
user_id: String(raw.user_id ?? "")
|
|
345
|
+
};
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
this._token = null;
|
|
349
|
+
return false;
|
|
350
|
+
} catch {
|
|
351
|
+
this._token = null;
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
function createAuthModule(bridge) {
|
|
357
|
+
return new HostAuthProvider(bridge);
|
|
358
|
+
}
|
|
359
|
+
|
|
302
360
|
// package.json
|
|
303
361
|
var package_default = {
|
|
304
362
|
name: "chatablex-web-sdk",
|
|
305
|
-
version: "1.0.
|
|
363
|
+
version: "1.0.31",
|
|
306
364
|
description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
307
365
|
main: "dist/index.js",
|
|
308
366
|
module: "dist/index.mjs",
|
|
@@ -401,7 +459,8 @@ var ChatableX = {
|
|
|
401
459
|
events: createEventsModule(bridge),
|
|
402
460
|
storage: createStorageModule(bridge),
|
|
403
461
|
tool: toolModule,
|
|
404
|
-
platform: createPlatformModule(bridge)
|
|
462
|
+
platform: createPlatformModule(bridge),
|
|
463
|
+
auth: createAuthModule(bridge)
|
|
405
464
|
};
|
|
406
465
|
window.ChatableX = sdk;
|
|
407
466
|
_instance = sdk;
|
package/dist/index.mjs
CHANGED
|
@@ -271,10 +271,68 @@ function createPlatformModule(bridge) {
|
|
|
271
271
|
};
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
+
// src/modules/auth.ts
|
|
275
|
+
var EXPIRY_SKEW_MS = 5e3;
|
|
276
|
+
function isValid(token, now) {
|
|
277
|
+
return !!token && typeof token.access_token === "string" && token.access_token.length > 0 && token.expires_at - EXPIRY_SKEW_MS > now;
|
|
278
|
+
}
|
|
279
|
+
var HostAuthProvider = class {
|
|
280
|
+
constructor(bridge) {
|
|
281
|
+
this._token = null;
|
|
282
|
+
/** In-flight refresh, so concurrent callers share one host round-trip. */
|
|
283
|
+
this._refreshing = null;
|
|
284
|
+
this._bridge = bridge;
|
|
285
|
+
}
|
|
286
|
+
async getToken() {
|
|
287
|
+
if (isValid(this._token, Date.now())) return this._token;
|
|
288
|
+
const ok = await this.refresh();
|
|
289
|
+
return ok ? this._token : null;
|
|
290
|
+
}
|
|
291
|
+
async getAuthHeaders() {
|
|
292
|
+
const token = await this.getToken();
|
|
293
|
+
if (!token) return {};
|
|
294
|
+
return { Authorization: `Bearer ${token.access_token}` };
|
|
295
|
+
}
|
|
296
|
+
getUserId() {
|
|
297
|
+
return this._token?.user_id ?? null;
|
|
298
|
+
}
|
|
299
|
+
isAuthenticated() {
|
|
300
|
+
return isValid(this._token, Date.now());
|
|
301
|
+
}
|
|
302
|
+
refresh() {
|
|
303
|
+
if (this._refreshing) return this._refreshing;
|
|
304
|
+
this._refreshing = this._doRefresh().finally(() => {
|
|
305
|
+
this._refreshing = null;
|
|
306
|
+
});
|
|
307
|
+
return this._refreshing;
|
|
308
|
+
}
|
|
309
|
+
async _doRefresh() {
|
|
310
|
+
try {
|
|
311
|
+
const raw = await this._bridge.sendMessage("host.getAuthToken");
|
|
312
|
+
if (raw && typeof raw === "object" && typeof raw.access_token === "string" && raw.access_token.length > 0) {
|
|
313
|
+
this._token = {
|
|
314
|
+
access_token: raw.access_token,
|
|
315
|
+
expires_at: Number(raw.expires_at) || 0,
|
|
316
|
+
user_id: String(raw.user_id ?? "")
|
|
317
|
+
};
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
this._token = null;
|
|
321
|
+
return false;
|
|
322
|
+
} catch {
|
|
323
|
+
this._token = null;
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
function createAuthModule(bridge) {
|
|
329
|
+
return new HostAuthProvider(bridge);
|
|
330
|
+
}
|
|
331
|
+
|
|
274
332
|
// package.json
|
|
275
333
|
var package_default = {
|
|
276
334
|
name: "chatablex-web-sdk",
|
|
277
|
-
version: "1.0.
|
|
335
|
+
version: "1.0.31",
|
|
278
336
|
description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
|
|
279
337
|
main: "dist/index.js",
|
|
280
338
|
module: "dist/index.mjs",
|
|
@@ -373,7 +431,8 @@ var ChatableX = {
|
|
|
373
431
|
events: createEventsModule(bridge),
|
|
374
432
|
storage: createStorageModule(bridge),
|
|
375
433
|
tool: toolModule,
|
|
376
|
-
platform: createPlatformModule(bridge)
|
|
434
|
+
platform: createPlatformModule(bridge),
|
|
435
|
+
auth: createAuthModule(bridge)
|
|
377
436
|
};
|
|
378
437
|
window.ChatableX = sdk;
|
|
379
438
|
_instance = sdk;
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { createUIModule } from './modules/ui';
|
|
|
26
26
|
import { createStorageModule } from './modules/storage';
|
|
27
27
|
import { createToolsModule } from './modules/tools';
|
|
28
28
|
import { createPlatformModule } from './modules/platform';
|
|
29
|
+
import { createAuthModule } from './modules/auth';
|
|
29
30
|
import type { ChatableXSDK, ChatableXInitConfig, ToolInfo } from './types';
|
|
30
31
|
import pkg from '../package.json';
|
|
31
32
|
|
|
@@ -86,6 +87,7 @@ export const ChatableX = {
|
|
|
86
87
|
storage: createStorageModule(bridge),
|
|
87
88
|
tool: toolModule,
|
|
88
89
|
platform: createPlatformModule(bridge),
|
|
90
|
+
auth: createAuthModule(bridge),
|
|
89
91
|
};
|
|
90
92
|
|
|
91
93
|
// Expose on window for debugging / Flutter interop
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import type { Bridge } from '../bridge';
|
|
2
|
+
import type { AuthTokenData, ChatableXAuth } from '../types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Strategy interface behind `sdk.auth`. Lets us swap how a token is obtained
|
|
6
|
+
* (hosted WebView today, browser/unified-login later) without touching any
|
|
7
|
+
* consumer code or the public `ChatableXAuth` surface.
|
|
8
|
+
*/
|
|
9
|
+
export interface AuthProvider extends ChatableXAuth {}
|
|
10
|
+
|
|
11
|
+
/** Treat a token as expired this many ms before its real `expires_at`. */
|
|
12
|
+
const EXPIRY_SKEW_MS = 5_000;
|
|
13
|
+
|
|
14
|
+
/** Shape of the host reply to `host.getAuthToken`. */
|
|
15
|
+
type HostAuthResponse =
|
|
16
|
+
| { access_token: string; expires_at: number; user_id: string; error?: undefined }
|
|
17
|
+
| { error: string; access_token?: undefined };
|
|
18
|
+
|
|
19
|
+
function isValid(token: AuthTokenData | null, now: number): token is AuthTokenData {
|
|
20
|
+
return !!token && typeof token.access_token === 'string' && token.access_token.length > 0
|
|
21
|
+
&& token.expires_at - EXPIRY_SKEW_MS > now;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Auth provider that reuses the desktop host's login session over the bridge.
|
|
26
|
+
* Token lives in memory only; the refresh_token never crosses the bridge —
|
|
27
|
+
* the host refreshes before sending (see FR-05).
|
|
28
|
+
*/
|
|
29
|
+
export class HostAuthProvider implements AuthProvider {
|
|
30
|
+
private _bridge: Bridge;
|
|
31
|
+
private _token: AuthTokenData | null = null;
|
|
32
|
+
/** In-flight refresh, so concurrent callers share one host round-trip. */
|
|
33
|
+
private _refreshing: Promise<boolean> | null = null;
|
|
34
|
+
|
|
35
|
+
constructor(bridge: Bridge) {
|
|
36
|
+
this._bridge = bridge;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async getToken(): Promise<AuthTokenData | null> {
|
|
40
|
+
if (isValid(this._token, Date.now())) return this._token;
|
|
41
|
+
const ok = await this.refresh();
|
|
42
|
+
return ok ? this._token : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async getAuthHeaders(): Promise<Record<string, string>> {
|
|
46
|
+
const token = await this.getToken();
|
|
47
|
+
if (!token) return {};
|
|
48
|
+
return { Authorization: `Bearer ${token.access_token}` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getUserId(): string | null {
|
|
52
|
+
return this._token?.user_id ?? null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isAuthenticated(): boolean {
|
|
56
|
+
return isValid(this._token, Date.now());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
refresh(): Promise<boolean> {
|
|
60
|
+
// single-flight: merge concurrent refreshes into one host round-trip
|
|
61
|
+
if (this._refreshing) return this._refreshing;
|
|
62
|
+
this._refreshing = this._doRefresh().finally(() => {
|
|
63
|
+
this._refreshing = null;
|
|
64
|
+
});
|
|
65
|
+
return this._refreshing;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private async _doRefresh(): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
const raw = (await this._bridge.sendMessage('host.getAuthToken')) as HostAuthResponse | null;
|
|
71
|
+
if (
|
|
72
|
+
raw &&
|
|
73
|
+
typeof raw === 'object' &&
|
|
74
|
+
typeof raw.access_token === 'string' &&
|
|
75
|
+
raw.access_token.length > 0
|
|
76
|
+
) {
|
|
77
|
+
this._token = {
|
|
78
|
+
access_token: raw.access_token,
|
|
79
|
+
expires_at: Number(raw.expires_at) || 0,
|
|
80
|
+
user_id: String(raw.user_id ?? ''),
|
|
81
|
+
};
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
// not authenticated / not hosted / host returned { error }
|
|
85
|
+
this._token = null;
|
|
86
|
+
return false;
|
|
87
|
+
} catch {
|
|
88
|
+
// bridge unavailable (non-WebView) or host error — degrade safely
|
|
89
|
+
this._token = null;
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Build the `auth` module. Selects the provider for the current environment;
|
|
97
|
+
* today there is only `HostAuthProvider`. A future `WebAuthProvider`
|
|
98
|
+
* (browser / unified login) can be slotted in here without changing callers.
|
|
99
|
+
*/
|
|
100
|
+
export function createAuthModule(bridge: Bridge): ChatableXAuth {
|
|
101
|
+
return new HostAuthProvider(bridge);
|
|
102
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -210,6 +210,47 @@ export interface ChatableXPlatform {
|
|
|
210
210
|
openInBrowser(targetUrl: string): Promise<void>;
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Auth
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/** Token payload returned by the host (never includes the refresh_token). */
|
|
218
|
+
export interface AuthTokenData {
|
|
219
|
+
/** Bearer access token to put in the Authorization header. */
|
|
220
|
+
access_token: string;
|
|
221
|
+
/** Access token expiry, epoch milliseconds. */
|
|
222
|
+
expires_at: number;
|
|
223
|
+
/** Authenticated user id. */
|
|
224
|
+
user_id: string;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Unified auth entry point for all WebUI apps.
|
|
229
|
+
*
|
|
230
|
+
* In a hosted (Flutter WebView) environment this reuses the desktop login
|
|
231
|
+
* session via the `host.getAuthToken` bridge call — apps never implement
|
|
232
|
+
* login or token handling themselves.
|
|
233
|
+
*/
|
|
234
|
+
export interface ChatableXAuth {
|
|
235
|
+
/**
|
|
236
|
+
* Get a valid access token. Returns the in-memory cached token when still
|
|
237
|
+
* valid, otherwise fetches a fresh one from the host. Returns `null` when
|
|
238
|
+
* not authenticated / not hosted.
|
|
239
|
+
*/
|
|
240
|
+
getToken(): Promise<AuthTokenData | null>;
|
|
241
|
+
/**
|
|
242
|
+
* Build auth headers ready to spread into a `fetch`. Returns
|
|
243
|
+
* `{ Authorization: "Bearer <token>" }` when authenticated, otherwise `{}`.
|
|
244
|
+
*/
|
|
245
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
246
|
+
/** Currently authenticated user id, or `null`. Synchronous (cache only). */
|
|
247
|
+
getUserId(): string | null;
|
|
248
|
+
/** Whether a valid token is currently cached. Synchronous (cache only). */
|
|
249
|
+
isAuthenticated(): boolean;
|
|
250
|
+
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
251
|
+
refresh(): Promise<boolean>;
|
|
252
|
+
}
|
|
253
|
+
|
|
213
254
|
export interface ChatableXSDK {
|
|
214
255
|
ai: ChatableXAI;
|
|
215
256
|
tools: ChatableXTools;
|
|
@@ -218,6 +259,7 @@ export interface ChatableXSDK {
|
|
|
218
259
|
storage: ChatableXStorage;
|
|
219
260
|
tool: ChatableXToolModule;
|
|
220
261
|
platform: ChatableXPlatform;
|
|
262
|
+
auth: ChatableXAuth;
|
|
221
263
|
}
|
|
222
264
|
|
|
223
265
|
// ---------------------------------------------------------------------------
|