@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/README.md +178 -0
- package/dist/index.cjs +1020 -0
- package/dist/index.d.cts +500 -0
- package/dist/index.d.ts +500 -0
- package/dist/index.js +986 -0
- package/package.json +40 -0
- package/src/client.test.ts +476 -0
- package/src/http.ts +224 -0
- package/src/index.ts +1278 -0
- package/tsconfig.json +19 -0
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
|
+
};
|