chatablex-web-sdk 1.0.31 → 1.0.32

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 CHANGED
@@ -34,6 +34,7 @@ Your WebUI runs inside a WebView. Many capabilities — native dialogs, file pic
34
34
  - [sdk.tools](#sdktools)
35
35
  - [sdk.platform](#sdkplatform)
36
36
  - [sdk.auth](#sdkauth)
37
+ - [sdk.cloud](#sdkcloud)
37
38
  - [Events Reference](#events-reference)
38
39
  - [Permissions](#permissions)
39
40
  - [Host Capability Matrix](#host-capability-matrix)
@@ -586,6 +587,65 @@ if (res.status === 401 && (await sdk.auth.refresh())) {
586
587
 
587
588
  ---
588
589
 
590
+ ### `sdk.cloud`
591
+
592
+ Unified cloud storage for **all** WebUI apps — keep user files saved across
593
+ devices without losing them. The SDK handles uploads, downloads, and retries;
594
+ you just call the methods. The `appId` (from `ChatableX.init`) is added
595
+ automatically, and each app's data is isolated so files never get mixed up.
596
+
597
+ > Requires: (1) the user is signed in (`sdk.auth`); (2) a configured cloud
598
+ > storage URL — pass `ChatableX.init({ apiBaseUrl })` (in a hosted environment
599
+ > ChatableX can also provide it automatically). Uploading is a **paid
600
+ > capability**: the user must purchase the corresponding tool before uploading,
601
+ > otherwise the upload is rejected.
602
+
603
+ | Method | Signature | Notes |
604
+ |--------|-----------|-------|
605
+ | `upload` | `(fileKey, data, opts?) => Promise<CloudUploadResult>` | Upload (overwrite). `data` accepts `Blob`/`ArrayBuffer`/`TypedArray`/`string`. |
606
+ | `download` | `(fileKey) => Promise<Blob>` | Download a file's bytes. |
607
+ | `getDownloadUrl` | `(fileKey) => Promise<string>` | Short-lived download URL (e.g. for `<img src>`). |
608
+ | `list` | `(opts?) => Promise<CloudFileInfo[]>` | List this app's files for the user; filter by `prefix`. |
609
+ | `delete` | `(fileKey) => Promise<void>` | Delete a file. |
610
+ | `usage` | `() => Promise<CloudUsage>` | Read the account's storage usage / quota. |
611
+
612
+ ```ts
613
+ const sdk = await ChatableX.init({
614
+ appId: 'math-studio',
615
+ apiBaseUrl: import.meta.env.VITE_CHATABLEX_API_BASE,
616
+ });
617
+
618
+ await sdk.cloud.upload('scenes/abc/scene.json.gz', blob, { contentType: 'application/gzip' });
619
+ const files = await sdk.cloud.list({ prefix: 'scenes/' });
620
+ const bytes = await sdk.cloud.download('scenes/abc/scene.json.gz');
621
+ await sdk.cloud.delete('scenes/abc/scene.json.gz');
622
+ const { usedBytes, quotaBytes } = await sdk.cloud.usage();
623
+ ```
624
+
625
+ **Error handling** (all `instanceof`-checkable, exported from the package root):
626
+
627
+ ```ts
628
+ import {
629
+ CloudAuthRequiredError, // not logged in
630
+ CloudSubscriptionRequiredError, // tool not purchased — prompt the user to buy it
631
+ CloudQuotaExceededError, // over storage quota; has usedBytes / quotaBytes
632
+ CloudError, // other cloud errors; has code
633
+ } from 'chatablex-web-sdk';
634
+ ```
635
+
636
+ **Behavior & guarantees**
637
+
638
+ - **Transparent auth.** Reuses `sdk.auth` internally; an expired session is
639
+ refreshed automatically and the request retried once. When the user is not
640
+ signed in it throws `CloudAuthRequiredError` and sends no request.
641
+ - **Data isolation.** Each user's and each app's data is isolated; apps and
642
+ users can never read each other's files.
643
+ - **Paid capability.** Uploading requires the user to have purchased the
644
+ corresponding tool; catch `CloudSubscriptionRequiredError` to drive a purchase
645
+ prompt.
646
+
647
+ ---
648
+
589
649
  ## Events Reference
590
650
 
591
651
  | Event | Payload | When fired |
@@ -630,6 +690,7 @@ SDK methods are thin RPC wrappers. Some host handlers are fully implemented; oth
630
690
  | `sdk.ui.updateState` | **Production** | Delegates to host |
631
691
  | `sdk.platform.openInBrowser` | **Production** | Auth handoff |
632
692
  | `sdk.auth.*` | **Production** | Hosted: reuses desktop login via `host.getAuthToken` (pre-refresh) |
693
+ | `sdk.cloud.*` | **Production** | Cloud file storage; needs `apiBaseUrl`, uploading requires purchasing the tool |
633
694
  | `sdk.ai.chat` | **Production** | Requires `ai_chat` + delegate |
634
695
  | `sdk.ai.getContext` | **Partial** | Returns minimal context |
635
696
  | `sdk.ai.chatStream` | **Partial** | Returns `{ streaming: true }`; tokens via events |
package/README.zh-CN.md CHANGED
@@ -34,6 +34,7 @@
34
34
  - [sdk.tools](#sdktools)
35
35
  - [sdk.platform](#sdkplatform)
36
36
  - [sdk.auth](#sdkauth)
37
+ - [sdk.cloud](#sdkcloud)
37
38
  - [事件参考](#事件参考)
38
39
  - [权限声明](#权限声明)
39
40
  - [宿主能力矩阵](#宿主能力矩阵)
@@ -585,6 +586,72 @@ if (res.status === 401 && (await sdk.auth.refresh())) {
585
586
 
586
587
  ---
587
588
 
589
+ ### `sdk.cloud`
590
+
591
+ 面向**所有** WebUI 应用的统一云存储,让用户文件跨设备保存、不丢失。上传、下载、
592
+ 重试等细节都由 SDK 处理,你只管调方法。`appId`(来自 `ChatableX.init`)会自动带上,
593
+ 每个应用的数据互相隔离,不会串。
594
+
595
+ > 需要:(1) 用户已登录(`sdk.auth`);(2) 已配置云存储服务地址——在
596
+ > `ChatableX.init({ apiBaseUrl })` 传入(宿主环境下也可由 ChatableX 自动提供)。
597
+ > 上传是**付费能力**:用户需购买对应工具后才能上传,未购买时上传会被拒绝。
598
+
599
+ | 方法 | 签名 | 说明 |
600
+ |------|------|------|
601
+ | `upload` | `(fileKey, data, opts?) => Promise<CloudUploadResult>` | 上传(覆盖写)。`data` 支持 `Blob`/`ArrayBuffer`/`TypedArray`/`string`。 |
602
+ | `download` | `(fileKey) => Promise<Blob>` | 下载文件字节。 |
603
+ | `getDownloadUrl` | `(fileKey) => Promise<string>` | 获取短期有效的下载链接(如喂给 `<img src>`)。 |
604
+ | `list` | `(opts?) => Promise<CloudFileInfo[]>` | 列出当前应用在该用户下的文件,可按 `prefix` 过滤。 |
605
+ | `delete` | `(fileKey) => Promise<void>` | 删除文件。 |
606
+ | `usage` | `() => Promise<CloudUsage>` | 读取账户存储用量/配额。 |
607
+
608
+ ```ts
609
+ const sdk = await ChatableX.init({
610
+ appId: 'math-studio',
611
+ apiBaseUrl: import.meta.env.VITE_CHATABLEX_API_BASE,
612
+ });
613
+
614
+ // 上传(app_id 自动注入,无需手动传)
615
+ await sdk.cloud.upload('scenes/abc/scene.json.gz', blob, { contentType: 'application/gzip' });
616
+
617
+ // 列表 / 下载 / 删除 / 用量
618
+ const files = await sdk.cloud.list({ prefix: 'scenes/' });
619
+ const bytes = await sdk.cloud.download('scenes/abc/scene.json.gz');
620
+ await sdk.cloud.delete('scenes/abc/scene.json.gz');
621
+ const { usedBytes, quotaBytes } = await sdk.cloud.usage();
622
+ ```
623
+
624
+ **错误处理**(均可 `instanceof` 判断,从包根导出):
625
+
626
+ ```ts
627
+ import {
628
+ CloudAuthRequiredError, // 未登录:提示登录 ChatableX
629
+ CloudSubscriptionRequiredError, // 未购买:引导用户购买工具
630
+ CloudQuotaExceededError, // 超出存储配额,含 usedBytes / quotaBytes
631
+ CloudError, // 其他云存储错误,含 code
632
+ } from 'chatablex-web-sdk';
633
+
634
+ try {
635
+ await sdk.cloud.upload('big.bin', buf);
636
+ } catch (e) {
637
+ if (e instanceof CloudSubscriptionRequiredError) {/* 弹出购买引导 */}
638
+ else if (e instanceof CloudQuotaExceededError) {/* 提示清理 / 升级,e.usedBytes/e.quotaBytes */}
639
+ else if (e instanceof CloudAuthRequiredError) {/* 提示登录 */}
640
+ else throw e;
641
+ }
642
+ ```
643
+
644
+ **行为与保证**
645
+
646
+ - **鉴权透明。** 内部复用 `sdk.auth`,登录态过期会自动刷新并重试一次;用户未登录
647
+ 时直接抛 `CloudAuthRequiredError`,不会发出无效请求。
648
+ - **数据隔离。** 每个用户、每个应用的数据互相隔离,应用之间、用户之间都不会读到
649
+ 对方的文件。
650
+ - **付费能力。** 上传需用户已购买对应工具;通过捕获
651
+ `CloudSubscriptionRequiredError` 引导用户购买。
652
+
653
+ ---
654
+
588
655
  ## 事件参考
589
656
 
590
657
  | 事件 | 载荷 | 触发时机 |
@@ -629,6 +696,7 @@ SDK 方法是薄 RPC 封装。部分宿主处理器已完整实现,部分返
629
696
  | `sdk.ui.updateState` | **生产可用** | 委托给宿主 |
630
697
  | `sdk.platform.openInBrowser` | **生产可用** | 鉴权传递 |
631
698
  | `sdk.auth.*` | **生产可用** | 宿主态:经 `host.getAuthToken` 复用桌面登录(下发前刷新) |
699
+ | `sdk.cloud.*` | **生产可用** | 云端文件存储;需配置 `apiBaseUrl`,上传需购买对应工具 |
632
700
  | `sdk.ai.chat` | **生产可用** | 需要 `ai_chat` + delegate |
633
701
  | `sdk.ai.getContext` | **部分实现** | 返回最小上下文 |
634
702
  | `sdk.ai.chatStream` | **部分实现** | 返回 `{ streaming: true }`;token 走事件 |
package/dist/index.d.mts CHANGED
@@ -119,6 +119,14 @@ interface ChatableXInitConfig {
119
119
  debug?: boolean;
120
120
  /** Timeout in ms for the handshake with Flutter (default: 10000) */
121
121
  timeout?: number;
122
+ /**
123
+ * Base URL of the ChatableX cloud API (auth-fc), e.g.
124
+ * `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
125
+ * `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
126
+ * the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
127
+ * calls reject with a clear error.
128
+ */
129
+ apiBaseUrl?: string;
122
130
  }
123
131
  interface ChatableXAI {
124
132
  chat(message: string, options?: ChatOptions): Promise<ChatResponse>;
@@ -191,6 +199,68 @@ interface ChatableXAuth {
191
199
  /** Force a token refresh via the host. Resolves `true` on success. */
192
200
  refresh(): Promise<boolean>;
193
201
  }
202
+ /** Binary payload accepted by `sdk.cloud.upload`. */
203
+ type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
204
+ interface CloudUploadOptions {
205
+ /**
206
+ * MIME type to store the object as. Defaults to the `Blob.type` when a Blob
207
+ * is given, otherwise `application/octet-stream`. Must be one allowed by the
208
+ * server's content-type whitelist.
209
+ */
210
+ contentType?: string;
211
+ }
212
+ /** Result of a successful `sdk.cloud.upload`. */
213
+ interface CloudUploadResult {
214
+ /** App-relative key (the same `fileKey` passed to `upload`). */
215
+ fileKey: string;
216
+ /** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
217
+ objectKey: string;
218
+ /** Bytes uploaded. */
219
+ size: number;
220
+ /** MIME type the object was stored as. */
221
+ contentType: string;
222
+ }
223
+ /** A single file in the user's cloud storage for this app. */
224
+ interface CloudFileInfo {
225
+ fileKey: string;
226
+ size: number;
227
+ /** ISO-8601 timestamp. */
228
+ lastModified: string;
229
+ }
230
+ interface CloudListOptions {
231
+ /** Restrict the listing to keys under this app-relative prefix. */
232
+ prefix?: string;
233
+ }
234
+ /** The user's storage usage / quota for the whole account. */
235
+ interface CloudUsage {
236
+ usedBytes: number;
237
+ quotaBytes: number;
238
+ fileCount: number;
239
+ /** ISO-8601 timestamp of the last server-side reconciliation, if any. */
240
+ reconciledAt?: string;
241
+ }
242
+ /**
243
+ * Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
244
+ * app's `appId` (from `ChatableX.init`) is injected automatically so every key
245
+ * is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
246
+ *
247
+ * Requires an authenticated session (`sdk.auth`) and a configured cloud API
248
+ * base URL (see `ChatableXInitConfig.apiBaseUrl`).
249
+ */
250
+ interface ChatableXCloud {
251
+ /** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
252
+ upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
253
+ /** Download a file's bytes as a Blob. */
254
+ download(fileKey: string): Promise<Blob>;
255
+ /** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
256
+ getDownloadUrl(fileKey: string): Promise<string>;
257
+ /** List the current app's files for this user. */
258
+ list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
259
+ /** Delete a file. Resolves even if the object did not exist. */
260
+ delete(fileKey: string): Promise<void>;
261
+ /** Read the account's storage usage / quota. */
262
+ usage(): Promise<CloudUsage>;
263
+ }
194
264
  interface ChatableXSDK {
195
265
  ai: ChatableXAI;
196
266
  tools: ChatableXTools;
@@ -200,6 +270,7 @@ interface ChatableXSDK {
200
270
  tool: ChatableXToolModule;
201
271
  platform: ChatableXPlatform;
202
272
  auth: ChatableXAuth;
273
+ cloud: ChatableXCloud;
203
274
  }
204
275
  declare global {
205
276
  interface Window {
@@ -246,6 +317,33 @@ declare class Bridge {
246
317
  destroy(): void;
247
318
  }
248
319
 
320
+ /** Base error for all `sdk.cloud` failures. */
321
+ declare class CloudError extends Error {
322
+ /** Business code from auth-fc (or the HTTP status when none was returned). */
323
+ readonly code: number;
324
+ constructor(message: string, code: number);
325
+ }
326
+ /**
327
+ * Thrown when no authenticated session is available. The app should prompt the
328
+ * user to log in to ChatableX before retrying.
329
+ */
330
+ declare class CloudAuthRequiredError extends CloudError {
331
+ constructor(message?: string);
332
+ }
333
+ /**
334
+ * Thrown when the user lacks the entitlement (purchased tool / membership)
335
+ * required to write to cloud storage. Maps to auth-fc code `40302`.
336
+ */
337
+ declare class CloudSubscriptionRequiredError extends CloudError {
338
+ constructor(message?: string);
339
+ }
340
+ /** Thrown when the upload would exceed the user's storage quota (code `40301`). */
341
+ declare class CloudQuotaExceededError extends CloudError {
342
+ readonly usedBytes: number;
343
+ readonly quotaBytes: number;
344
+ constructor(usedBytes: number, quotaBytes: number, message?: string);
345
+ }
346
+
249
347
  /**
250
348
  * chatablex-web-sdk
251
349
  *
@@ -288,4 +386,4 @@ declare const ChatableX: {
288
386
  version: string;
289
387
  };
290
388
 
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 };
389
+ export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXCloud, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, CloudAuthRequiredError, CloudError, type CloudFileInfo, type CloudListOptions, CloudQuotaExceededError, CloudSubscriptionRequiredError, type CloudUploadData, type CloudUploadOptions, type CloudUploadResult, type CloudUsage, 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
@@ -119,6 +119,14 @@ interface ChatableXInitConfig {
119
119
  debug?: boolean;
120
120
  /** Timeout in ms for the handshake with Flutter (default: 10000) */
121
121
  timeout?: number;
122
+ /**
123
+ * Base URL of the ChatableX cloud API (auth-fc), e.g.
124
+ * `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
125
+ * `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
126
+ * the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
127
+ * calls reject with a clear error.
128
+ */
129
+ apiBaseUrl?: string;
122
130
  }
123
131
  interface ChatableXAI {
124
132
  chat(message: string, options?: ChatOptions): Promise<ChatResponse>;
@@ -191,6 +199,68 @@ interface ChatableXAuth {
191
199
  /** Force a token refresh via the host. Resolves `true` on success. */
192
200
  refresh(): Promise<boolean>;
193
201
  }
202
+ /** Binary payload accepted by `sdk.cloud.upload`. */
203
+ type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
204
+ interface CloudUploadOptions {
205
+ /**
206
+ * MIME type to store the object as. Defaults to the `Blob.type` when a Blob
207
+ * is given, otherwise `application/octet-stream`. Must be one allowed by the
208
+ * server's content-type whitelist.
209
+ */
210
+ contentType?: string;
211
+ }
212
+ /** Result of a successful `sdk.cloud.upload`. */
213
+ interface CloudUploadResult {
214
+ /** App-relative key (the same `fileKey` passed to `upload`). */
215
+ fileKey: string;
216
+ /** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
217
+ objectKey: string;
218
+ /** Bytes uploaded. */
219
+ size: number;
220
+ /** MIME type the object was stored as. */
221
+ contentType: string;
222
+ }
223
+ /** A single file in the user's cloud storage for this app. */
224
+ interface CloudFileInfo {
225
+ fileKey: string;
226
+ size: number;
227
+ /** ISO-8601 timestamp. */
228
+ lastModified: string;
229
+ }
230
+ interface CloudListOptions {
231
+ /** Restrict the listing to keys under this app-relative prefix. */
232
+ prefix?: string;
233
+ }
234
+ /** The user's storage usage / quota for the whole account. */
235
+ interface CloudUsage {
236
+ usedBytes: number;
237
+ quotaBytes: number;
238
+ fileCount: number;
239
+ /** ISO-8601 timestamp of the last server-side reconciliation, if any. */
240
+ reconciledAt?: string;
241
+ }
242
+ /**
243
+ * Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
244
+ * app's `appId` (from `ChatableX.init`) is injected automatically so every key
245
+ * is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
246
+ *
247
+ * Requires an authenticated session (`sdk.auth`) and a configured cloud API
248
+ * base URL (see `ChatableXInitConfig.apiBaseUrl`).
249
+ */
250
+ interface ChatableXCloud {
251
+ /** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
252
+ upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
253
+ /** Download a file's bytes as a Blob. */
254
+ download(fileKey: string): Promise<Blob>;
255
+ /** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
256
+ getDownloadUrl(fileKey: string): Promise<string>;
257
+ /** List the current app's files for this user. */
258
+ list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
259
+ /** Delete a file. Resolves even if the object did not exist. */
260
+ delete(fileKey: string): Promise<void>;
261
+ /** Read the account's storage usage / quota. */
262
+ usage(): Promise<CloudUsage>;
263
+ }
194
264
  interface ChatableXSDK {
195
265
  ai: ChatableXAI;
196
266
  tools: ChatableXTools;
@@ -200,6 +270,7 @@ interface ChatableXSDK {
200
270
  tool: ChatableXToolModule;
201
271
  platform: ChatableXPlatform;
202
272
  auth: ChatableXAuth;
273
+ cloud: ChatableXCloud;
203
274
  }
204
275
  declare global {
205
276
  interface Window {
@@ -246,6 +317,33 @@ declare class Bridge {
246
317
  destroy(): void;
247
318
  }
248
319
 
320
+ /** Base error for all `sdk.cloud` failures. */
321
+ declare class CloudError extends Error {
322
+ /** Business code from auth-fc (or the HTTP status when none was returned). */
323
+ readonly code: number;
324
+ constructor(message: string, code: number);
325
+ }
326
+ /**
327
+ * Thrown when no authenticated session is available. The app should prompt the
328
+ * user to log in to ChatableX before retrying.
329
+ */
330
+ declare class CloudAuthRequiredError extends CloudError {
331
+ constructor(message?: string);
332
+ }
333
+ /**
334
+ * Thrown when the user lacks the entitlement (purchased tool / membership)
335
+ * required to write to cloud storage. Maps to auth-fc code `40302`.
336
+ */
337
+ declare class CloudSubscriptionRequiredError extends CloudError {
338
+ constructor(message?: string);
339
+ }
340
+ /** Thrown when the upload would exceed the user's storage quota (code `40301`). */
341
+ declare class CloudQuotaExceededError extends CloudError {
342
+ readonly usedBytes: number;
343
+ readonly quotaBytes: number;
344
+ constructor(usedBytes: number, quotaBytes: number, message?: string);
345
+ }
346
+
249
347
  /**
250
348
  * chatablex-web-sdk
251
349
  *
@@ -288,4 +386,4 @@ declare const ChatableX: {
288
386
  version: string;
289
387
  };
290
388
 
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 };
389
+ export { type AiResponseEventData, type AuthTokenData, Bridge, type ChatOptions, type ChatResponse, ChatableX, type ChatableXAI, type ChatableXAuth, type ChatableXCloud, type ChatableXEvents, type ChatableXInitConfig, type ChatableXPlatform, type ChatableXSDK, type ChatableXStorage, type ChatableXToolModule, type ChatableXTools, type ChatableXUI, type CloseEventData, CloudAuthRequiredError, CloudError, type CloudFileInfo, type CloudListOptions, CloudQuotaExceededError, CloudSubscriptionRequiredError, type CloudUploadData, type CloudUploadOptions, type CloudUploadResult, type CloudUsage, 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
@@ -22,6 +22,10 @@ var index_exports = {};
22
22
  __export(index_exports, {
23
23
  Bridge: () => Bridge,
24
24
  ChatableX: () => ChatableX,
25
+ CloudAuthRequiredError: () => CloudAuthRequiredError,
26
+ CloudError: () => CloudError,
27
+ CloudQuotaExceededError: () => CloudQuotaExceededError,
28
+ CloudSubscriptionRequiredError: () => CloudSubscriptionRequiredError,
25
29
  SDK_VERSION: () => SDK_VERSION
26
30
  });
27
31
  module.exports = __toCommonJS(index_exports);
@@ -357,10 +361,188 @@ function createAuthModule(bridge) {
357
361
  return new HostAuthProvider(bridge);
358
362
  }
359
363
 
364
+ // src/modules/cloud.ts
365
+ var CloudError = class extends Error {
366
+ constructor(message, code) {
367
+ super(message);
368
+ this.name = "CloudError";
369
+ this.code = code;
370
+ }
371
+ };
372
+ var CloudAuthRequiredError = class extends CloudError {
373
+ constructor(message = "cloud storage requires an authenticated session") {
374
+ super(message, 401);
375
+ this.name = "CloudAuthRequiredError";
376
+ }
377
+ };
378
+ var CloudSubscriptionRequiredError = class extends CloudError {
379
+ constructor(message = "cloud storage requires an active subscription") {
380
+ super(message, 40302);
381
+ this.name = "CloudSubscriptionRequiredError";
382
+ }
383
+ };
384
+ var CloudQuotaExceededError = class extends CloudError {
385
+ constructor(usedBytes, quotaBytes, message = "storage quota exceeded") {
386
+ super(message, 40301);
387
+ this.name = "CloudQuotaExceededError";
388
+ this.usedBytes = usedBytes;
389
+ this.quotaBytes = quotaBytes;
390
+ }
391
+ };
392
+ var DEFAULT_CONTENT_TYPE = "application/octet-stream";
393
+ function normalizeData(data) {
394
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
395
+ return { body: data, size: data.size, type: data.type || "" };
396
+ }
397
+ if (typeof data === "string") {
398
+ const blob = new Blob([data]);
399
+ return { body: blob, size: blob.size, type: "" };
400
+ }
401
+ if (data instanceof ArrayBuffer) {
402
+ return { body: data, size: data.byteLength, type: "" };
403
+ }
404
+ if (ArrayBuffer.isView(data)) {
405
+ return { body: data, size: data.byteLength, type: "" };
406
+ }
407
+ throw new CloudError("unsupported upload data type", 400);
408
+ }
409
+ function createCloudModule(bridge, deps) {
410
+ const { appId, auth } = deps;
411
+ let resolvedBase = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
412
+ function stripTrailingSlash(url) {
413
+ return url.replace(/\/+$/, "");
414
+ }
415
+ async function baseUrl() {
416
+ if (resolvedBase) return resolvedBase;
417
+ try {
418
+ const r = await bridge.sendMessage("host.getApiBaseUrl", {}, 5e3);
419
+ const url = typeof r === "string" ? r : r && typeof r === "object" && typeof r.base_url === "string" ? r.base_url : "";
420
+ if (url) {
421
+ resolvedBase = stripTrailingSlash(url);
422
+ return resolvedBase;
423
+ }
424
+ } catch {
425
+ }
426
+ throw new CloudError(
427
+ "cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()",
428
+ 0
429
+ );
430
+ }
431
+ async function authedFetch(path, init) {
432
+ const token = await auth.getToken();
433
+ if (!token) throw new CloudAuthRequiredError();
434
+ const base = await baseUrl();
435
+ const url = `${base}${path}`;
436
+ const build = async () => ({
437
+ ...init,
438
+ headers: { ...init.headers ?? {}, ...await auth.getAuthHeaders() }
439
+ });
440
+ let res = await fetch(url, await build());
441
+ if (res.status === 401 && await auth.refresh()) {
442
+ res = await fetch(url, await build());
443
+ }
444
+ return res;
445
+ }
446
+ async function callApi(path, init) {
447
+ const res = await authedFetch(path, init);
448
+ let body = null;
449
+ try {
450
+ body = await res.json();
451
+ } catch {
452
+ }
453
+ const code = body?.code;
454
+ const message = body?.message || `HTTP ${res.status}`;
455
+ if (!res.ok || body?.success === false) {
456
+ if (code === 40301) {
457
+ const q = body?.data ?? {};
458
+ throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
459
+ }
460
+ if (code === 40302) throw new CloudSubscriptionRequiredError(message);
461
+ if (res.status === 401) throw new CloudAuthRequiredError(message);
462
+ throw new CloudError(message, code ?? res.status);
463
+ }
464
+ return body?.data ?? null;
465
+ }
466
+ function validateFileKey(fileKey) {
467
+ if (!fileKey || typeof fileKey !== "string") {
468
+ throw new CloudError("fileKey is required", 400);
469
+ }
470
+ }
471
+ return {
472
+ async upload(fileKey, data, options = {}) {
473
+ validateFileKey(fileKey);
474
+ const { body, size, type } = normalizeData(data);
475
+ const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
476
+ const signed = await callApi("/api/storage/upload-url", {
477
+ method: "POST",
478
+ headers: { "Content-Type": "application/json" },
479
+ body: JSON.stringify({
480
+ app_id: appId,
481
+ file_key: fileKey,
482
+ content_type: contentType,
483
+ size_bytes: size
484
+ })
485
+ });
486
+ const put = await fetch(signed.upload_url, {
487
+ method: "PUT",
488
+ headers: { "Content-Type": contentType },
489
+ body
490
+ });
491
+ if (!put.ok) {
492
+ throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
493
+ }
494
+ return { fileKey, objectKey: signed.object_key, size, contentType };
495
+ },
496
+ async getDownloadUrl(fileKey) {
497
+ validateFileKey(fileKey);
498
+ const signed = await callApi("/api/storage/download-url", {
499
+ method: "POST",
500
+ headers: { "Content-Type": "application/json" },
501
+ body: JSON.stringify({ app_id: appId, file_key: fileKey })
502
+ });
503
+ return signed.download_url;
504
+ },
505
+ async download(fileKey) {
506
+ const url = await this.getDownloadUrl(fileKey);
507
+ const res = await fetch(url, { method: "GET" });
508
+ if (!res.ok) {
509
+ throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
510
+ }
511
+ return res.blob();
512
+ },
513
+ async list(options = {}) {
514
+ const qs = new URLSearchParams({ app_id: appId });
515
+ if (options.prefix) qs.set("prefix", options.prefix);
516
+ const data = await callApi(`/api/storage/files?${qs.toString()}`, {
517
+ method: "GET"
518
+ });
519
+ return (data?.files ?? []).map((f) => ({
520
+ fileKey: f.file_key,
521
+ size: f.size,
522
+ lastModified: f.last_modified
523
+ }));
524
+ },
525
+ async delete(fileKey) {
526
+ validateFileKey(fileKey);
527
+ const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
528
+ await callApi(`/api/storage/files?${qs.toString()}`, { method: "DELETE" });
529
+ },
530
+ async usage() {
531
+ const data = await callApi("/api/storage/usage", { method: "GET" });
532
+ return {
533
+ usedBytes: data.used_bytes,
534
+ quotaBytes: data.quota_bytes,
535
+ fileCount: data.file_count,
536
+ reconciledAt: data.reconciled_at
537
+ };
538
+ }
539
+ };
540
+ }
541
+
360
542
  // package.json
361
543
  var package_default = {
362
544
  name: "chatablex-web-sdk",
363
- version: "1.0.31",
545
+ version: "1.0.32",
364
546
  description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
365
547
  main: "dist/index.js",
366
548
  module: "dist/index.mjs",
@@ -452,6 +634,7 @@ var ChatableX = {
452
634
  }
453
635
  const toolModule = createToolModule(bridge, config.appId);
454
636
  if (toolConfig) toolModule._setInfo(toolConfig);
637
+ const authModule = createAuthModule(bridge);
455
638
  const sdk = {
456
639
  ai: createAIModule(bridge),
457
640
  tools: createToolsModule(bridge),
@@ -460,7 +643,12 @@ var ChatableX = {
460
643
  storage: createStorageModule(bridge),
461
644
  tool: toolModule,
462
645
  platform: createPlatformModule(bridge),
463
- auth: createAuthModule(bridge)
646
+ auth: authModule,
647
+ cloud: createCloudModule(bridge, {
648
+ appId: config.appId,
649
+ auth: authModule,
650
+ apiBaseUrl: config.apiBaseUrl
651
+ })
464
652
  };
465
653
  window.ChatableX = sdk;
466
654
  _instance = sdk;
@@ -483,5 +671,9 @@ var ChatableX = {
483
671
  0 && (module.exports = {
484
672
  Bridge,
485
673
  ChatableX,
674
+ CloudAuthRequiredError,
675
+ CloudError,
676
+ CloudQuotaExceededError,
677
+ CloudSubscriptionRequiredError,
486
678
  SDK_VERSION
487
679
  });
package/dist/index.mjs CHANGED
@@ -329,10 +329,188 @@ function createAuthModule(bridge) {
329
329
  return new HostAuthProvider(bridge);
330
330
  }
331
331
 
332
+ // src/modules/cloud.ts
333
+ var CloudError = class extends Error {
334
+ constructor(message, code) {
335
+ super(message);
336
+ this.name = "CloudError";
337
+ this.code = code;
338
+ }
339
+ };
340
+ var CloudAuthRequiredError = class extends CloudError {
341
+ constructor(message = "cloud storage requires an authenticated session") {
342
+ super(message, 401);
343
+ this.name = "CloudAuthRequiredError";
344
+ }
345
+ };
346
+ var CloudSubscriptionRequiredError = class extends CloudError {
347
+ constructor(message = "cloud storage requires an active subscription") {
348
+ super(message, 40302);
349
+ this.name = "CloudSubscriptionRequiredError";
350
+ }
351
+ };
352
+ var CloudQuotaExceededError = class extends CloudError {
353
+ constructor(usedBytes, quotaBytes, message = "storage quota exceeded") {
354
+ super(message, 40301);
355
+ this.name = "CloudQuotaExceededError";
356
+ this.usedBytes = usedBytes;
357
+ this.quotaBytes = quotaBytes;
358
+ }
359
+ };
360
+ var DEFAULT_CONTENT_TYPE = "application/octet-stream";
361
+ function normalizeData(data) {
362
+ if (typeof Blob !== "undefined" && data instanceof Blob) {
363
+ return { body: data, size: data.size, type: data.type || "" };
364
+ }
365
+ if (typeof data === "string") {
366
+ const blob = new Blob([data]);
367
+ return { body: blob, size: blob.size, type: "" };
368
+ }
369
+ if (data instanceof ArrayBuffer) {
370
+ return { body: data, size: data.byteLength, type: "" };
371
+ }
372
+ if (ArrayBuffer.isView(data)) {
373
+ return { body: data, size: data.byteLength, type: "" };
374
+ }
375
+ throw new CloudError("unsupported upload data type", 400);
376
+ }
377
+ function createCloudModule(bridge, deps) {
378
+ const { appId, auth } = deps;
379
+ let resolvedBase = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
380
+ function stripTrailingSlash(url) {
381
+ return url.replace(/\/+$/, "");
382
+ }
383
+ async function baseUrl() {
384
+ if (resolvedBase) return resolvedBase;
385
+ try {
386
+ const r = await bridge.sendMessage("host.getApiBaseUrl", {}, 5e3);
387
+ const url = typeof r === "string" ? r : r && typeof r === "object" && typeof r.base_url === "string" ? r.base_url : "";
388
+ if (url) {
389
+ resolvedBase = stripTrailingSlash(url);
390
+ return resolvedBase;
391
+ }
392
+ } catch {
393
+ }
394
+ throw new CloudError(
395
+ "cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()",
396
+ 0
397
+ );
398
+ }
399
+ async function authedFetch(path, init) {
400
+ const token = await auth.getToken();
401
+ if (!token) throw new CloudAuthRequiredError();
402
+ const base = await baseUrl();
403
+ const url = `${base}${path}`;
404
+ const build = async () => ({
405
+ ...init,
406
+ headers: { ...init.headers ?? {}, ...await auth.getAuthHeaders() }
407
+ });
408
+ let res = await fetch(url, await build());
409
+ if (res.status === 401 && await auth.refresh()) {
410
+ res = await fetch(url, await build());
411
+ }
412
+ return res;
413
+ }
414
+ async function callApi(path, init) {
415
+ const res = await authedFetch(path, init);
416
+ let body = null;
417
+ try {
418
+ body = await res.json();
419
+ } catch {
420
+ }
421
+ const code = body?.code;
422
+ const message = body?.message || `HTTP ${res.status}`;
423
+ if (!res.ok || body?.success === false) {
424
+ if (code === 40301) {
425
+ const q = body?.data ?? {};
426
+ throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
427
+ }
428
+ if (code === 40302) throw new CloudSubscriptionRequiredError(message);
429
+ if (res.status === 401) throw new CloudAuthRequiredError(message);
430
+ throw new CloudError(message, code ?? res.status);
431
+ }
432
+ return body?.data ?? null;
433
+ }
434
+ function validateFileKey(fileKey) {
435
+ if (!fileKey || typeof fileKey !== "string") {
436
+ throw new CloudError("fileKey is required", 400);
437
+ }
438
+ }
439
+ return {
440
+ async upload(fileKey, data, options = {}) {
441
+ validateFileKey(fileKey);
442
+ const { body, size, type } = normalizeData(data);
443
+ const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
444
+ const signed = await callApi("/api/storage/upload-url", {
445
+ method: "POST",
446
+ headers: { "Content-Type": "application/json" },
447
+ body: JSON.stringify({
448
+ app_id: appId,
449
+ file_key: fileKey,
450
+ content_type: contentType,
451
+ size_bytes: size
452
+ })
453
+ });
454
+ const put = await fetch(signed.upload_url, {
455
+ method: "PUT",
456
+ headers: { "Content-Type": contentType },
457
+ body
458
+ });
459
+ if (!put.ok) {
460
+ throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
461
+ }
462
+ return { fileKey, objectKey: signed.object_key, size, contentType };
463
+ },
464
+ async getDownloadUrl(fileKey) {
465
+ validateFileKey(fileKey);
466
+ const signed = await callApi("/api/storage/download-url", {
467
+ method: "POST",
468
+ headers: { "Content-Type": "application/json" },
469
+ body: JSON.stringify({ app_id: appId, file_key: fileKey })
470
+ });
471
+ return signed.download_url;
472
+ },
473
+ async download(fileKey) {
474
+ const url = await this.getDownloadUrl(fileKey);
475
+ const res = await fetch(url, { method: "GET" });
476
+ if (!res.ok) {
477
+ throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
478
+ }
479
+ return res.blob();
480
+ },
481
+ async list(options = {}) {
482
+ const qs = new URLSearchParams({ app_id: appId });
483
+ if (options.prefix) qs.set("prefix", options.prefix);
484
+ const data = await callApi(`/api/storage/files?${qs.toString()}`, {
485
+ method: "GET"
486
+ });
487
+ return (data?.files ?? []).map((f) => ({
488
+ fileKey: f.file_key,
489
+ size: f.size,
490
+ lastModified: f.last_modified
491
+ }));
492
+ },
493
+ async delete(fileKey) {
494
+ validateFileKey(fileKey);
495
+ const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
496
+ await callApi(`/api/storage/files?${qs.toString()}`, { method: "DELETE" });
497
+ },
498
+ async usage() {
499
+ const data = await callApi("/api/storage/usage", { method: "GET" });
500
+ return {
501
+ usedBytes: data.used_bytes,
502
+ quotaBytes: data.quota_bytes,
503
+ fileCount: data.file_count,
504
+ reconciledAt: data.reconciled_at
505
+ };
506
+ }
507
+ };
508
+ }
509
+
332
510
  // package.json
333
511
  var package_default = {
334
512
  name: "chatablex-web-sdk",
335
- version: "1.0.31",
513
+ version: "1.0.32",
336
514
  description: "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
337
515
  main: "dist/index.js",
338
516
  module: "dist/index.mjs",
@@ -424,6 +602,7 @@ var ChatableX = {
424
602
  }
425
603
  const toolModule = createToolModule(bridge, config.appId);
426
604
  if (toolConfig) toolModule._setInfo(toolConfig);
605
+ const authModule = createAuthModule(bridge);
427
606
  const sdk = {
428
607
  ai: createAIModule(bridge),
429
608
  tools: createToolsModule(bridge),
@@ -432,7 +611,12 @@ var ChatableX = {
432
611
  storage: createStorageModule(bridge),
433
612
  tool: toolModule,
434
613
  platform: createPlatformModule(bridge),
435
- auth: createAuthModule(bridge)
614
+ auth: authModule,
615
+ cloud: createCloudModule(bridge, {
616
+ appId: config.appId,
617
+ auth: authModule,
618
+ apiBaseUrl: config.apiBaseUrl
619
+ })
436
620
  };
437
621
  window.ChatableX = sdk;
438
622
  _instance = sdk;
@@ -454,5 +638,9 @@ var ChatableX = {
454
638
  export {
455
639
  Bridge,
456
640
  ChatableX,
641
+ CloudAuthRequiredError,
642
+ CloudError,
643
+ CloudQuotaExceededError,
644
+ CloudSubscriptionRequiredError,
457
645
  SDK_VERSION
458
646
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chatablex-web-sdk",
3
- "version": "1.0.31",
3
+ "version": "1.0.32",
4
4
  "description": "ChatableX Web SDK for AI App WebUI development. Provides bridge communication with the ChatableX Flutter client.",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
package/src/index.ts CHANGED
@@ -27,6 +27,7 @@ import { createStorageModule } from './modules/storage';
27
27
  import { createToolsModule } from './modules/tools';
28
28
  import { createPlatformModule } from './modules/platform';
29
29
  import { createAuthModule } from './modules/auth';
30
+ import { createCloudModule } from './modules/cloud';
30
31
  import type { ChatableXSDK, ChatableXInitConfig, ToolInfo } from './types';
31
32
  import pkg from '../package.json';
32
33
 
@@ -79,6 +80,8 @@ export const ChatableX = {
79
80
  const toolModule = createToolModule(bridge, config.appId);
80
81
  if (toolConfig) toolModule._setInfo(toolConfig);
81
82
 
83
+ const authModule = createAuthModule(bridge);
84
+
82
85
  const sdk: ChatableXSDK = {
83
86
  ai: createAIModule(bridge),
84
87
  tools: createToolsModule(bridge),
@@ -87,7 +90,12 @@ export const ChatableX = {
87
90
  storage: createStorageModule(bridge),
88
91
  tool: toolModule,
89
92
  platform: createPlatformModule(bridge),
90
- auth: createAuthModule(bridge),
93
+ auth: authModule,
94
+ cloud: createCloudModule(bridge, {
95
+ appId: config.appId,
96
+ auth: authModule,
97
+ apiBaseUrl: config.apiBaseUrl,
98
+ }),
91
99
  };
92
100
 
93
101
  // Expose on window for debugging / Flutter interop
@@ -116,3 +124,9 @@ export const ChatableX = {
116
124
  // Re-export all types
117
125
  export * from './types';
118
126
  export { Bridge } from './bridge';
127
+ export {
128
+ CloudError,
129
+ CloudAuthRequiredError,
130
+ CloudSubscriptionRequiredError,
131
+ CloudQuotaExceededError,
132
+ } from './modules/cloud';
@@ -0,0 +1,297 @@
1
+ import type { Bridge } from '../bridge';
2
+ import type {
3
+ ChatableXAuth,
4
+ ChatableXCloud,
5
+ CloudFileInfo,
6
+ CloudListOptions,
7
+ CloudUploadData,
8
+ CloudUploadOptions,
9
+ CloudUploadResult,
10
+ CloudUsage,
11
+ } from '../types';
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Errors (instanceof-checkable so apps can branch their UI)
15
+ // ---------------------------------------------------------------------------
16
+
17
+ /** Base error for all `sdk.cloud` failures. */
18
+ export class CloudError extends Error {
19
+ /** Business code from auth-fc (or the HTTP status when none was returned). */
20
+ readonly code: number;
21
+ constructor(message: string, code: number) {
22
+ super(message);
23
+ this.name = 'CloudError';
24
+ this.code = code;
25
+ }
26
+ }
27
+
28
+ /**
29
+ * Thrown when no authenticated session is available. The app should prompt the
30
+ * user to log in to ChatableX before retrying.
31
+ */
32
+ export class CloudAuthRequiredError extends CloudError {
33
+ constructor(message = 'cloud storage requires an authenticated session') {
34
+ super(message, 401);
35
+ this.name = 'CloudAuthRequiredError';
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Thrown when the user lacks the entitlement (purchased tool / membership)
41
+ * required to write to cloud storage. Maps to auth-fc code `40302`.
42
+ */
43
+ export class CloudSubscriptionRequiredError extends CloudError {
44
+ constructor(message = 'cloud storage requires an active subscription') {
45
+ super(message, 40302);
46
+ this.name = 'CloudSubscriptionRequiredError';
47
+ }
48
+ }
49
+
50
+ /** Thrown when the upload would exceed the user's storage quota (code `40301`). */
51
+ export class CloudQuotaExceededError extends CloudError {
52
+ readonly usedBytes: number;
53
+ readonly quotaBytes: number;
54
+ constructor(usedBytes: number, quotaBytes: number, message = 'storage quota exceeded') {
55
+ super(message, 40301);
56
+ this.name = 'CloudQuotaExceededError';
57
+ this.usedBytes = usedBytes;
58
+ this.quotaBytes = quotaBytes;
59
+ }
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Internal helpers
64
+ // ---------------------------------------------------------------------------
65
+
66
+ const DEFAULT_CONTENT_TYPE = 'application/octet-stream';
67
+
68
+ /** auth-fc response envelope: { success, code, message, data }. */
69
+ interface ApiEnvelope<T> {
70
+ success?: boolean;
71
+ code?: number;
72
+ message?: string;
73
+ data?: T;
74
+ }
75
+
76
+ interface UploadURLData {
77
+ upload_url: string;
78
+ object_key: string;
79
+ expires_in: number;
80
+ }
81
+ interface DownloadURLData {
82
+ download_url: string;
83
+ object_key: string;
84
+ expires_in: number;
85
+ }
86
+ interface ListFilesData {
87
+ files: Array<{ file_key: string; size: number; last_modified: string }>;
88
+ total: number;
89
+ }
90
+ interface UsageData {
91
+ used_bytes: number;
92
+ quota_bytes: number;
93
+ file_count: number;
94
+ reconciled_at?: string;
95
+ }
96
+ interface QuotaErrorData {
97
+ used_bytes?: number;
98
+ quota_bytes?: number;
99
+ }
100
+
101
+ function normalizeData(data: CloudUploadData): { body: BodyInit; size: number; type: string } {
102
+ if (typeof Blob !== 'undefined' && data instanceof Blob) {
103
+ return { body: data, size: data.size, type: data.type || '' };
104
+ }
105
+ if (typeof data === 'string') {
106
+ const blob = new Blob([data]);
107
+ return { body: blob, size: blob.size, type: '' };
108
+ }
109
+ if (data instanceof ArrayBuffer) {
110
+ return { body: data, size: data.byteLength, type: '' };
111
+ }
112
+ if (ArrayBuffer.isView(data)) {
113
+ return { body: data as unknown as BodyInit, size: data.byteLength, type: '' };
114
+ }
115
+ throw new CloudError('unsupported upload data type', 400);
116
+ }
117
+
118
+ export interface CloudModuleDeps {
119
+ appId: string;
120
+ auth: ChatableXAuth;
121
+ /** Explicit cloud API base URL (overrides the host-provided one). */
122
+ apiBaseUrl?: string;
123
+ }
124
+
125
+ export function createCloudModule(bridge: Bridge, deps: CloudModuleDeps): ChatableXCloud {
126
+ const { appId, auth } = deps;
127
+ let resolvedBase: string | null = deps.apiBaseUrl ? stripTrailingSlash(deps.apiBaseUrl) : null;
128
+
129
+ function stripTrailingSlash(url: string): string {
130
+ return url.replace(/\/+$/, '');
131
+ }
132
+
133
+ /** Resolve the cloud API base URL: explicit config > host bridge > error. */
134
+ async function baseUrl(): Promise<string> {
135
+ if (resolvedBase) return resolvedBase;
136
+ try {
137
+ // Short timeout: a hosted-but-unimplemented method shouldn't hang the call.
138
+ const r = await bridge.sendMessage('host.getApiBaseUrl', {}, 5_000);
139
+ const url =
140
+ typeof r === 'string'
141
+ ? r
142
+ : r && typeof r === 'object' && typeof (r as { base_url?: unknown }).base_url === 'string'
143
+ ? (r as { base_url: string }).base_url
144
+ : '';
145
+ if (url) {
146
+ resolvedBase = stripTrailingSlash(url);
147
+ return resolvedBase;
148
+ }
149
+ } catch {
150
+ // host doesn't implement it / not hosted — fall through to error
151
+ }
152
+ throw new CloudError(
153
+ 'cloud API base URL is not configured; pass apiBaseUrl to ChatableX.init()',
154
+ 0,
155
+ );
156
+ }
157
+
158
+ /**
159
+ * fetch against auth-fc that injects the host login session and retries once
160
+ * on 401 after letting `sdk.auth` refresh. Rejects with CloudAuthRequiredError
161
+ * when there is no valid token (no unauthenticated request is sent).
162
+ */
163
+ async function authedFetch(path: string, init: RequestInit): Promise<Response> {
164
+ const token = await auth.getToken();
165
+ if (!token) throw new CloudAuthRequiredError();
166
+
167
+ const base = await baseUrl();
168
+ const url = `${base}${path}`;
169
+
170
+ const build = async (): Promise<RequestInit> => ({
171
+ ...init,
172
+ headers: { ...(init.headers ?? {}), ...(await auth.getAuthHeaders()) },
173
+ });
174
+
175
+ let res = await fetch(url, await build());
176
+ if (res.status === 401 && (await auth.refresh())) {
177
+ res = await fetch(url, await build());
178
+ }
179
+ return res;
180
+ }
181
+
182
+ /** Call an auth-fc JSON endpoint and unwrap the `data` payload. */
183
+ async function callApi<T>(path: string, init: RequestInit): Promise<T> {
184
+ const res = await authedFetch(path, init);
185
+
186
+ let body: ApiEnvelope<unknown> | null = null;
187
+ try {
188
+ body = (await res.json()) as ApiEnvelope<unknown>;
189
+ } catch {
190
+ // non-JSON body
191
+ }
192
+
193
+ const code = body?.code;
194
+ const message = body?.message || `HTTP ${res.status}`;
195
+
196
+ if (!res.ok || body?.success === false) {
197
+ if (code === 40301) {
198
+ const q = (body?.data ?? {}) as QuotaErrorData;
199
+ throw new CloudQuotaExceededError(q.used_bytes ?? 0, q.quota_bytes ?? 0, message);
200
+ }
201
+ if (code === 40302) throw new CloudSubscriptionRequiredError(message);
202
+ if (res.status === 401) throw new CloudAuthRequiredError(message);
203
+ throw new CloudError(message, code ?? res.status);
204
+ }
205
+
206
+ return (body?.data ?? null) as T;
207
+ }
208
+
209
+ function validateFileKey(fileKey: string): void {
210
+ if (!fileKey || typeof fileKey !== 'string') {
211
+ throw new CloudError('fileKey is required', 400);
212
+ }
213
+ }
214
+
215
+ return {
216
+ async upload(
217
+ fileKey: string,
218
+ data: CloudUploadData,
219
+ options: CloudUploadOptions = {},
220
+ ): Promise<CloudUploadResult> {
221
+ validateFileKey(fileKey);
222
+ const { body, size, type } = normalizeData(data);
223
+ const contentType = options.contentType || type || DEFAULT_CONTENT_TYPE;
224
+
225
+ const signed = await callApi<UploadURLData>('/api/storage/upload-url', {
226
+ method: 'POST',
227
+ headers: { 'Content-Type': 'application/json' },
228
+ body: JSON.stringify({
229
+ app_id: appId,
230
+ file_key: fileKey,
231
+ content_type: contentType,
232
+ size_bytes: size,
233
+ }),
234
+ });
235
+
236
+ // Direct client → OSS PUT. Content-Type MUST match what was signed.
237
+ const put = await fetch(signed.upload_url, {
238
+ method: 'PUT',
239
+ headers: { 'Content-Type': contentType },
240
+ body,
241
+ });
242
+ if (!put.ok) {
243
+ throw new CloudError(`OSS upload failed: HTTP ${put.status}`, put.status);
244
+ }
245
+
246
+ return { fileKey, objectKey: signed.object_key, size, contentType };
247
+ },
248
+
249
+ async getDownloadUrl(fileKey: string): Promise<string> {
250
+ validateFileKey(fileKey);
251
+ const signed = await callApi<DownloadURLData>('/api/storage/download-url', {
252
+ method: 'POST',
253
+ headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ app_id: appId, file_key: fileKey }),
255
+ });
256
+ return signed.download_url;
257
+ },
258
+
259
+ async download(fileKey: string): Promise<Blob> {
260
+ const url = await this.getDownloadUrl(fileKey);
261
+ const res = await fetch(url, { method: 'GET' });
262
+ if (!res.ok) {
263
+ throw new CloudError(`OSS download failed: HTTP ${res.status}`, res.status);
264
+ }
265
+ return res.blob();
266
+ },
267
+
268
+ async list(options: CloudListOptions = {}): Promise<CloudFileInfo[]> {
269
+ const qs = new URLSearchParams({ app_id: appId });
270
+ if (options.prefix) qs.set('prefix', options.prefix);
271
+ const data = await callApi<ListFilesData>(`/api/storage/files?${qs.toString()}`, {
272
+ method: 'GET',
273
+ });
274
+ return (data?.files ?? []).map((f) => ({
275
+ fileKey: f.file_key,
276
+ size: f.size,
277
+ lastModified: f.last_modified,
278
+ }));
279
+ },
280
+
281
+ async delete(fileKey: string): Promise<void> {
282
+ validateFileKey(fileKey);
283
+ const qs = new URLSearchParams({ app_id: appId, file_key: fileKey });
284
+ await callApi<unknown>(`/api/storage/files?${qs.toString()}`, { method: 'DELETE' });
285
+ },
286
+
287
+ async usage(): Promise<CloudUsage> {
288
+ const data = await callApi<UsageData>('/api/storage/usage', { method: 'GET' });
289
+ return {
290
+ usedBytes: data.used_bytes,
291
+ quotaBytes: data.quota_bytes,
292
+ fileCount: data.file_count,
293
+ reconciledAt: data.reconciled_at,
294
+ };
295
+ },
296
+ };
297
+ }
package/src/types.ts CHANGED
@@ -161,6 +161,14 @@ export interface ChatableXInitConfig {
161
161
  debug?: boolean;
162
162
  /** Timeout in ms for the handshake with Flutter (default: 10000) */
163
163
  timeout?: number;
164
+ /**
165
+ * Base URL of the ChatableX cloud API (auth-fc), e.g.
166
+ * `https://chatabl-fc-prod-xxxx.cn-hangzhou.fcapp.run`. Required for
167
+ * `sdk.cloud` to work. When omitted, the SDK best-effort asks the host via
168
+ * the `host.getApiBaseUrl` bridge call; if that is also unavailable, cloud
169
+ * calls reject with a clear error.
170
+ */
171
+ apiBaseUrl?: string;
164
172
  }
165
173
 
166
174
  // ---------------------------------------------------------------------------
@@ -251,6 +259,79 @@ export interface ChatableXAuth {
251
259
  refresh(): Promise<boolean>;
252
260
  }
253
261
 
262
+ // ---------------------------------------------------------------------------
263
+ // Cloud Storage
264
+ // ---------------------------------------------------------------------------
265
+
266
+ /** Binary payload accepted by `sdk.cloud.upload`. */
267
+ export type CloudUploadData = Blob | ArrayBuffer | ArrayBufferView | string;
268
+
269
+ export interface CloudUploadOptions {
270
+ /**
271
+ * MIME type to store the object as. Defaults to the `Blob.type` when a Blob
272
+ * is given, otherwise `application/octet-stream`. Must be one allowed by the
273
+ * server's content-type whitelist.
274
+ */
275
+ contentType?: string;
276
+ }
277
+
278
+ /** Result of a successful `sdk.cloud.upload`. */
279
+ export interface CloudUploadResult {
280
+ /** App-relative key (the same `fileKey` passed to `upload`). */
281
+ fileKey: string;
282
+ /** Fully-qualified OSS object key (`user-data/{user_id}/{app_id}/{fileKey}`). */
283
+ objectKey: string;
284
+ /** Bytes uploaded. */
285
+ size: number;
286
+ /** MIME type the object was stored as. */
287
+ contentType: string;
288
+ }
289
+
290
+ /** A single file in the user's cloud storage for this app. */
291
+ export interface CloudFileInfo {
292
+ fileKey: string;
293
+ size: number;
294
+ /** ISO-8601 timestamp. */
295
+ lastModified: string;
296
+ }
297
+
298
+ export interface CloudListOptions {
299
+ /** Restrict the listing to keys under this app-relative prefix. */
300
+ prefix?: string;
301
+ }
302
+
303
+ /** The user's storage usage / quota for the whole account. */
304
+ export interface CloudUsage {
305
+ usedBytes: number;
306
+ quotaBytes: number;
307
+ fileCount: number;
308
+ /** ISO-8601 timestamp of the last server-side reconciliation, if any. */
309
+ reconciledAt?: string;
310
+ }
311
+
312
+ /**
313
+ * Cloud storage for WebUI apps. Backed by auth-fc presigned OSS URLs; the
314
+ * app's `appId` (from `ChatableX.init`) is injected automatically so every key
315
+ * is namespaced to `{user}/{app}` and apps cannot reach into each other's data.
316
+ *
317
+ * Requires an authenticated session (`sdk.auth`) and a configured cloud API
318
+ * base URL (see `ChatableXInitConfig.apiBaseUrl`).
319
+ */
320
+ export interface ChatableXCloud {
321
+ /** Upload (overwrite) a file. Resolves once the bytes are stored in OSS. */
322
+ upload(fileKey: string, data: CloudUploadData, options?: CloudUploadOptions): Promise<CloudUploadResult>;
323
+ /** Download a file's bytes as a Blob. */
324
+ download(fileKey: string): Promise<Blob>;
325
+ /** Get a short-lived presigned GET URL (e.g. to feed an `<img src>`). */
326
+ getDownloadUrl(fileKey: string): Promise<string>;
327
+ /** List the current app's files for this user. */
328
+ list(options?: CloudListOptions): Promise<CloudFileInfo[]>;
329
+ /** Delete a file. Resolves even if the object did not exist. */
330
+ delete(fileKey: string): Promise<void>;
331
+ /** Read the account's storage usage / quota. */
332
+ usage(): Promise<CloudUsage>;
333
+ }
334
+
254
335
  export interface ChatableXSDK {
255
336
  ai: ChatableXAI;
256
337
  tools: ChatableXTools;
@@ -260,6 +341,7 @@ export interface ChatableXSDK {
260
341
  tool: ChatableXToolModule;
261
342
  platform: ChatableXPlatform;
262
343
  auth: ChatableXAuth;
344
+ cloud: ChatableXCloud;
263
345
  }
264
346
 
265
347
  // ---------------------------------------------------------------------------