chatablex-web-sdk 1.0.3 → 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 +115 -0
- package/README.zh-CN.md +121 -0
- package/dist/index.d.mts +135 -1
- package/dist/index.d.ts +135 -1
- package/dist/index.js +253 -2
- package/dist/index.mjs +249 -2
- package/package.json +1 -1
- package/src/index.ts +16 -0
- package/src/modules/auth.ts +102 -0
- package/src/modules/cloud.ts +297 -0
- package/src/types.ts +124 -0
|
@@ -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
|
+
}
|
|
@@ -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
|
// ---------------------------------------------------------------------------
|
|
@@ -210,6 +218,120 @@ export interface ChatableXPlatform {
|
|
|
210
218
|
openInBrowser(targetUrl: string): Promise<void>;
|
|
211
219
|
}
|
|
212
220
|
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Auth
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** Token payload returned by the host (never includes the refresh_token). */
|
|
226
|
+
export interface AuthTokenData {
|
|
227
|
+
/** Bearer access token to put in the Authorization header. */
|
|
228
|
+
access_token: string;
|
|
229
|
+
/** Access token expiry, epoch milliseconds. */
|
|
230
|
+
expires_at: number;
|
|
231
|
+
/** Authenticated user id. */
|
|
232
|
+
user_id: string;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Unified auth entry point for all WebUI apps.
|
|
237
|
+
*
|
|
238
|
+
* In a hosted (Flutter WebView) environment this reuses the desktop login
|
|
239
|
+
* session via the `host.getAuthToken` bridge call — apps never implement
|
|
240
|
+
* login or token handling themselves.
|
|
241
|
+
*/
|
|
242
|
+
export interface ChatableXAuth {
|
|
243
|
+
/**
|
|
244
|
+
* Get a valid access token. Returns the in-memory cached token when still
|
|
245
|
+
* valid, otherwise fetches a fresh one from the host. Returns `null` when
|
|
246
|
+
* not authenticated / not hosted.
|
|
247
|
+
*/
|
|
248
|
+
getToken(): Promise<AuthTokenData | null>;
|
|
249
|
+
/**
|
|
250
|
+
* Build auth headers ready to spread into a `fetch`. Returns
|
|
251
|
+
* `{ Authorization: "Bearer <token>" }` when authenticated, otherwise `{}`.
|
|
252
|
+
*/
|
|
253
|
+
getAuthHeaders(): Promise<Record<string, string>>;
|
|
254
|
+
/** Currently authenticated user id, or `null`. Synchronous (cache only). */
|
|
255
|
+
getUserId(): string | null;
|
|
256
|
+
/** Whether a valid token is currently cached. Synchronous (cache only). */
|
|
257
|
+
isAuthenticated(): boolean;
|
|
258
|
+
/** Force a token refresh via the host. Resolves `true` on success. */
|
|
259
|
+
refresh(): Promise<boolean>;
|
|
260
|
+
}
|
|
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
|
+
|
|
213
335
|
export interface ChatableXSDK {
|
|
214
336
|
ai: ChatableXAI;
|
|
215
337
|
tools: ChatableXTools;
|
|
@@ -218,6 +340,8 @@ export interface ChatableXSDK {
|
|
|
218
340
|
storage: ChatableXStorage;
|
|
219
341
|
tool: ChatableXToolModule;
|
|
220
342
|
platform: ChatableXPlatform;
|
|
343
|
+
auth: ChatableXAuth;
|
|
344
|
+
cloud: ChatableXCloud;
|
|
221
345
|
}
|
|
222
346
|
|
|
223
347
|
// ---------------------------------------------------------------------------
|