@55387.ai/uniauth-client 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/http.ts ADDED
@@ -0,0 +1,224 @@
1
+ /**
2
+ * HTTP Utilities for UniAuth Client SDK
3
+ * UniAuth 客户端 SDK HTTP 工具
4
+ *
5
+ * Provides robust HTTP request handling with:
6
+ * - Automatic retry with exponential backoff
7
+ * - Timeout handling
8
+ * - Request/response interceptors
9
+ */
10
+
11
+ /**
12
+ * HTTP fetch options with retry configuration
13
+ */
14
+ export interface FetchWithRetryOptions extends RequestInit {
15
+ /** Maximum number of retry attempts (default: 3) */
16
+ maxRetries?: number;
17
+ /** Base delay in ms between retries (default: 500) */
18
+ baseDelay?: number;
19
+ /** Request timeout in ms (default: 30000) */
20
+ timeout?: number;
21
+ /** HTTP status codes that should trigger a retry (default: [408, 429, 500, 502, 503, 504]) */
22
+ retryStatusCodes?: number[];
23
+ }
24
+
25
+ /**
26
+ * Default retry status codes
27
+ * These status codes indicate temporary failures that may succeed on retry
28
+ */
29
+ const DEFAULT_RETRY_STATUS_CODES = [
30
+ 408, // Request Timeout
31
+ 429, // Too Many Requests
32
+ 500, // Internal Server Error
33
+ 502, // Bad Gateway
34
+ 503, // Service Unavailable
35
+ 504, // Gateway Timeout
36
+ ];
37
+
38
+ /**
39
+ * Fetch with automatic retry and exponential backoff
40
+ * 带自动重试和指数退避的 fetch
41
+ *
42
+ * @param url - The URL to fetch
43
+ * @param options - Fetch options with retry configuration
44
+ * @returns The fetch response
45
+ * @throws Error if all retries fail
46
+ */
47
+ export async function fetchWithRetry(
48
+ url: string,
49
+ options: FetchWithRetryOptions = {}
50
+ ): Promise<Response> {
51
+ const {
52
+ maxRetries = 3,
53
+ baseDelay = 500,
54
+ timeout = 30000,
55
+ retryStatusCodes = DEFAULT_RETRY_STATUS_CODES,
56
+ ...fetchOptions
57
+ } = options;
58
+
59
+ let lastError: Error | null = null;
60
+
61
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
62
+ try {
63
+ // Create abort controller for timeout
64
+ const controller = new AbortController();
65
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
66
+
67
+ const response = await fetch(url, {
68
+ ...fetchOptions,
69
+ signal: controller.signal,
70
+ });
71
+
72
+ clearTimeout(timeoutId);
73
+
74
+ // Check if we should retry based on status code
75
+ if (retryStatusCodes.includes(response.status) && attempt < maxRetries) {
76
+ // Check for Retry-After header
77
+ const retryAfter = response.headers.get('Retry-After');
78
+ const delay = retryAfter
79
+ ? parseRetryAfter(retryAfter)
80
+ : calculateBackoffDelay(attempt, baseDelay);
81
+
82
+ await sleep(delay);
83
+ continue;
84
+ }
85
+
86
+ return response;
87
+ } catch (error) {
88
+ lastError = error instanceof Error ? error : new Error(String(error));
89
+
90
+ // Don't retry on abort (intentional cancellation)
91
+ if (lastError.name === 'AbortError' && attempt >= maxRetries) {
92
+ throw new Error(`Request timeout after ${timeout}ms`);
93
+ }
94
+
95
+ // Check if we should retry
96
+ if (attempt < maxRetries) {
97
+ const delay = calculateBackoffDelay(attempt, baseDelay);
98
+ await sleep(delay);
99
+ }
100
+ }
101
+ }
102
+
103
+ throw lastError || new Error('Request failed after all retries');
104
+ }
105
+
106
+ /**
107
+ * Calculate exponential backoff delay with jitter
108
+ * 计算带抖动的指数退避延迟
109
+ *
110
+ * @param attempt - Current attempt number (0-indexed)
111
+ * @param baseDelay - Base delay in milliseconds
112
+ * @returns Delay in milliseconds
113
+ */
114
+ function calculateBackoffDelay(attempt: number, baseDelay: number): number {
115
+ // Exponential backoff: baseDelay * 2^attempt
116
+ const exponentialDelay = baseDelay * Math.pow(2, attempt);
117
+
118
+ // Add random jitter (±25%) to prevent thundering herd
119
+ const jitter = exponentialDelay * 0.25 * (Math.random() * 2 - 1);
120
+
121
+ // Cap at 30 seconds
122
+ return Math.min(exponentialDelay + jitter, 30000);
123
+ }
124
+
125
+ /**
126
+ * Parse Retry-After header value
127
+ * 解析 Retry-After 头部值
128
+ *
129
+ * @param value - Retry-After header value (seconds or HTTP-date)
130
+ * @returns Delay in milliseconds
131
+ */
132
+ function parseRetryAfter(value: string): number {
133
+ // If it's a number of seconds
134
+ const seconds = parseInt(value, 10);
135
+ if (!isNaN(seconds)) {
136
+ return seconds * 1000;
137
+ }
138
+
139
+ // If it's an HTTP-date
140
+ const date = new Date(value);
141
+ if (!isNaN(date.getTime())) {
142
+ const delay = date.getTime() - Date.now();
143
+ return Math.max(delay, 0);
144
+ }
145
+
146
+ // Default to 1 second
147
+ return 1000;
148
+ }
149
+
150
+ /**
151
+ * Sleep for a specified duration
152
+ * 休眠指定时间
153
+ */
154
+ function sleep(ms: number): Promise<void> {
155
+ return new Promise(resolve => setTimeout(resolve, ms));
156
+ }
157
+
158
+ /**
159
+ * PKCE (Proof Key for Code Exchange) utilities
160
+ * PKCE 工具函数
161
+ */
162
+
163
+ /**
164
+ * Generate a cryptographically random code verifier
165
+ * 生成加密随机的 code_verifier
166
+ */
167
+ export function generateCodeVerifier(): string {
168
+ const array = new Uint8Array(32);
169
+ crypto.getRandomValues(array);
170
+ return base64UrlEncode(array);
171
+ }
172
+
173
+ /**
174
+ * Generate code challenge from verifier using SHA-256
175
+ * 使用 SHA-256 从 verifier 生成 code_challenge
176
+ */
177
+ export async function generateCodeChallenge(verifier: string): Promise<string> {
178
+ const encoder = new TextEncoder();
179
+ const data = encoder.encode(verifier);
180
+ const digest = await crypto.subtle.digest('SHA-256', data);
181
+ return base64UrlEncode(new Uint8Array(digest));
182
+ }
183
+
184
+ /**
185
+ * Base64 URL encode a Uint8Array
186
+ * Base64 URL 编码
187
+ */
188
+ function base64UrlEncode(array: Uint8Array): string {
189
+ let binary = '';
190
+ for (let i = 0; i < array.byteLength; i++) {
191
+ binary += String.fromCharCode(array[i]);
192
+ }
193
+ return btoa(binary)
194
+ .replace(/\+/g, '-')
195
+ .replace(/\//g, '_')
196
+ .replace(/=+$/, '');
197
+ }
198
+
199
+ /**
200
+ * Store PKCE code verifier for later use
201
+ * 存储 PKCE code_verifier 以供后续使用
202
+ */
203
+ export function storeCodeVerifier(verifier: string, storageKey = 'uniauth_pkce_verifier'): void {
204
+ if (typeof sessionStorage !== 'undefined') {
205
+ sessionStorage.setItem(storageKey, verifier);
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Retrieve and clear stored PKCE code verifier
211
+ * 获取并清除存储的 PKCE code_verifier
212
+ */
213
+ export function getAndClearCodeVerifier(storageKey = 'uniauth_pkce_verifier'): string | null {
214
+ if (typeof sessionStorage !== 'undefined') {
215
+ const verifier = sessionStorage.getItem(storageKey);
216
+ sessionStorage.removeItem(storageKey);
217
+ return verifier;
218
+ }
219
+ return null;
220
+ }
221
+
222
+ export {
223
+ DEFAULT_RETRY_STATUS_CODES,
224
+ };