Download apps/web/src/api/native.ts from hunian003/message: direct link, hf CLI and curl.
- Browser
- Download file 9.09 kB
-
https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/api/native.ts
- Command line
-
hf download hf://spaces/hunian003/message/apps/web/src/api/native.ts
-
curl -L -o native.ts https://huggingface.co/spaces/hunian003/message/resolve/main/apps/web/src/api/native.ts
9.09 kB
| import type { | |
| NativeArtifact, NativeOperation, NativeOperationKind, NativeOperationReceipt, | |
| NativeProfile, NativeProfileSummary, NativeAuthStatus, NativeAuthSession, NativeClientKey, NativeAuditEvent, | |
| } from '@typings/native'; | |
| export class NativeApiError extends Error { | |
| constructor(public readonly status: number, message: string) { | |
| super(message); | |
| this.name = 'NativeApiError'; | |
| } | |
| } | |
| /** HttpOnly 会话 cookie 仅由服务器设置;CSRF 只放在内存,不写入浏览器存储。 */ | |
| export class NativeApi { | |
| private refreshing?: Promise<NativeAuthStatus>; | |
| private loggingOut = false; | |
| constructor(private csrfToken?: string) {} | |
| private refreshSession() { | |
| // 多个并行管理请求遇到过期 access cookie 时共用一次恢复,避免刷新凭据互相覆盖。 | |
| this.refreshing ??= this.authStatus().then(status => { | |
| if (status.authenticated && status.csrf_token) this.csrfToken = status.csrf_token; | |
| return status; | |
| }).finally(() => { this.refreshing = undefined; }); | |
| return this.refreshing; | |
| } | |
| private async request<T>(path: string, options: RequestInit = {}, binary = false, secrets: string[] = [], retried = false): Promise<T> { | |
| const attemptedCsrf = this.csrfToken; | |
| const controller = new AbortController(); | |
| const abort = () => controller.abort(); | |
| options.signal?.addEventListener('abort', abort, { once: true }); | |
| if (options.signal?.aborted) controller.abort(); | |
| const timer = setTimeout(abort, 30000); | |
| try { | |
| const headers = new Headers(options.headers); | |
| // 状态变更需要同时提交会话 cookie 和 CSRF,绝不发送管理员 bearer token。 | |
| if (options.method && !['GET', 'HEAD', 'OPTIONS'].includes(options.method.toUpperCase())) { | |
| if (!this.csrfToken) throw new NativeApiError(403, '缺少会话校验信息,请刷新页面后重试'); | |
| headers.set('X-CSRF-Token', this.csrfToken); | |
| } | |
| if (options.body && !(options.body instanceof FormData)) headers.set('Content-Type', 'application/json'); | |
| const response = await fetch(`/api/native${path}`, { | |
| ...options, headers, signal: controller.signal, credentials: 'same-origin', cache: 'no-store', | |
| }); | |
| // 此标记只由读取请求体之前的CSRF门禁生成;原请求尚未产生副作用,最多重试一次。 | |
| if (response.status === 403 && response.headers.get('X-Message-Error') === 'csrf' && !retried) { | |
| const status = await this.authStatus(options.signal ?? undefined); | |
| if (!status.csrf_token) throw new NativeApiError(403, '无法取得登录校验信息,请重新打开独立应用页面'); | |
| this.csrfToken = status.csrf_token; | |
| await this.ensureCsrf(options.signal ?? undefined); | |
| return this.request<T>(path, options, binary, secrets, true); | |
| } | |
| // 仅重试鉴权层拒绝的 401;网络失败和业务错误绝不自动重放写操作。 | |
| if (response.status === 401 && !retried && !this.loggingOut && !['/auth/status', '/auth/login', '/auth/recover', '/auth/password'].includes(path)) { | |
| const status = this.csrfToken !== attemptedCsrf | |
| ? { authenticated: true, csrf_token: this.csrfToken } : await this.refreshSession(); | |
| if (status.authenticated && status.csrf_token && !controller.signal.aborted) { | |
| this.csrfToken = status.csrf_token; | |
| return this.request<T>(path, options, binary, secrets, true); | |
| } | |
| } | |
| if (!response.ok) { | |
| let detail = `请求失败(HTTP ${response.status})`; | |
| try { | |
| const body: unknown = await response.json(); | |
| if (body && typeof body === 'object' && 'detail' in body && typeof body.detail === 'string') detail = body.detail; | |
| } catch { /* 错误页不一定是 JSON,保留状态码即可诊断。 */ } | |
| // 即使错误响应意外回显密码或 CSRF,也不在界面中展示。 | |
| for (const secret of [this.csrfToken, attemptedCsrf, ...secrets]) { | |
| if (secret) detail = detail.split(secret).join('[已隐藏凭据]'); | |
| } | |
| throw new NativeApiError(response.status, detail); | |
| } | |
| if (response.status === 204) return undefined as T; | |
| return (binary ? await response.blob() : await response.json()) as T; | |
| } catch (error) { | |
| if (error instanceof NativeApiError) throw error; | |
| if (controller.signal.aborted) throw new NativeApiError(0, '请求已取消或超时;操作可能已被服务器接收,请刷新历史确认'); | |
| throw new NativeApiError(0, '网络请求失败,请检查连接并重试;操作结果以服务器记录为准'); | |
| } finally { | |
| clearTimeout(timer); | |
| options.signal?.removeEventListener('abort', abort); | |
| } | |
| } | |
| async ensureCsrf(signal?: AbortSignal) { | |
| const probe = () => this.request<{ ready: boolean }>('/auth/csrf', { signal, headers: { 'X-CSRF-Token': this.csrfToken ?? '' } }); | |
| try { return await probe(); } | |
| catch (error) { | |
| if (!(error instanceof NativeApiError) || error.status !== 409) throw error; | |
| // 多窗口或并发状态响应可使页面CSRF过时,只重取校验,不重发邮件或密码。 | |
| const status = await this.authStatus(signal); | |
| this.csrfToken = status.csrf_token; | |
| return probe(); | |
| } | |
| } | |
| authStatus(signal?: AbortSignal) { return this.request<NativeAuthStatus>('/auth/status', { signal }); } | |
| login(email: string, password: string, signal?: AbortSignal) { | |
| return this.request<NativeAuthSession>('/auth/login', { | |
| method: 'POST', body: JSON.stringify({ email, password }), signal, | |
| }, false, [password]); | |
| } | |
| recover(email: string, signal?: AbortSignal) { | |
| return this.request<{ message?: string }>('/auth/recover', { | |
| method: 'POST', body: JSON.stringify({ email }), signal, | |
| }); | |
| } | |
| updatePassword(password: string, signal?: AbortSignal) { | |
| return this.request<{ message?: string }>('/auth/password', { | |
| method: 'POST', body: JSON.stringify({ password }), signal, | |
| }, false, [password]); | |
| } | |
| async logout() { | |
| // 已启动的刷新先结束,再撤销服务器会话,避免较晚的刷新响应覆盖退出 cookie。 | |
| this.loggingOut = true; | |
| try { await this.refreshing; return await this.request<void>('/auth/logout', { method: 'POST' }); } | |
| finally { this.loggingOut = false; } | |
| } | |
| keys(profile: string, signal?: AbortSignal) { | |
| return this.request<{ items: NativeClientKey[] }>(`/profiles/${encodeURIComponent(profile)}/keys`, { signal }); | |
| } | |
| createKey(profile: string, name: string, expiresInDays: number, signal?: AbortSignal) { | |
| return this.request<NativeClientKey & { key: string }>(`/profiles/${encodeURIComponent(profile)}/keys`, { | |
| method: 'POST', body: JSON.stringify({ name, expires_in_days: expiresInDays }), signal, | |
| }); | |
| } | |
| revokeKey(profile: string, keyId: string, signal?: AbortSignal) { | |
| return this.request<unknown>(`/profiles/${encodeURIComponent(profile)}/keys/${encodeURIComponent(keyId)}`, { method: 'DELETE', signal }); | |
| } | |
| audit(signal?: AbortSignal) { return this.request<{ items: NativeAuditEvent[] }>('/auth/audit', { signal }); } | |
| catalog(signal?: AbortSignal) { return this.request<{ items: NativeArtifact[] }>('/catalog', { signal }); } | |
| profiles(signal?: AbortSignal) { return this.request<{ items: NativeProfileSummary[] }>('/profiles', { signal }); } | |
| profile(id: string, signal?: AbortSignal) { return this.request<NativeProfile>(`/profiles/${encodeURIComponent(id)}`, { signal }); } | |
| createProfile(id: string, signal?: AbortSignal) { | |
| return this.request<NativeProfileSummary>('/profiles', { method: 'POST', body: JSON.stringify({ id }), signal }); | |
| } | |
| upload(file: File, signal?: AbortSignal) { | |
| const body = new FormData(); | |
| body.append('file', file); | |
| return this.request<NativeArtifact>('/catalog', { method: 'POST', body, signal }); | |
| } | |
| download(id: string, signal?: AbortSignal) { | |
| return this.request<Blob>(`/catalog/${encodeURIComponent(id)}/download`, { signal }, true); | |
| } | |
| operate(id: string, kind: NativeOperationKind, body: Record<string, unknown> | undefined, key: string) { | |
| return this.request<NativeOperationReceipt>(`/profiles/${encodeURIComponent(id)}/${kind}`, { | |
| method: 'POST', body: body ? JSON.stringify(body) : undefined, headers: { 'Idempotency-Key': key }, | |
| }); | |
| } | |
| operations(profile: string, signal?: AbortSignal) { | |
| return this.request<{ items: NativeOperation[] }>(`/operations?profile_id=${encodeURIComponent(profile)}`, { signal }); | |
| } | |
| operation(id: string, signal?: AbortSignal) { | |
| return this.request<NativeOperation>(`/operations/${encodeURIComponent(id)}`, { signal }); | |
| } | |
| cancel(id: string) { return this.request<NativeOperation>(`/operations/${encodeURIComponent(id)}/cancel`, { method: 'POST' }); } | |
| call(id: string, name: string, args: Record<string, unknown>, signal?: AbortSignal) { | |
| return this.request<unknown>(`/profiles/${encodeURIComponent(id)}/call`, { | |
| method: 'POST', body: JSON.stringify({ name, arguments: args }), signal, | |
| }); | |
| } | |
| } | |