accessio 1.3.0 → 1.5.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.
Files changed (39) hide show
  1. package/cjs/accessio.cjs +98 -97
  2. package/cjs/accessio.cjs.map +1 -1
  3. package/cjs/core/accessioError.cjs +51 -1
  4. package/cjs/core/accessioError.cjs.map +1 -1
  5. package/cjs/core/buildURL.cjs +10 -4
  6. package/cjs/core/buildURL.cjs.map +1 -1
  7. package/cjs/core/fetchAdapter.cjs +125 -105
  8. package/cjs/core/fetchAdapter.cjs.map +1 -1
  9. package/cjs/core/request.cjs +73 -23
  10. package/cjs/core/request.cjs.map +1 -1
  11. package/cjs/core/retry.cjs +8 -5
  12. package/cjs/core/retry.cjs.map +1 -1
  13. package/cjs/helpers/debug.cjs +7 -1
  14. package/cjs/helpers/debug.cjs.map +1 -1
  15. package/cjs/helpers/flattenHeaders.cjs +4 -1
  16. package/cjs/helpers/flattenHeaders.cjs.map +1 -1
  17. package/cjs/helpers/rateLimiter.cjs +31 -3
  18. package/cjs/helpers/rateLimiter.cjs.map +1 -1
  19. package/cjs/helpers/settle.cjs +1 -1
  20. package/cjs/helpers/settle.cjs.map +1 -1
  21. package/cjs/helpers/toFormData.cjs +1 -1
  22. package/cjs/helpers/toFormData.cjs.map +1 -1
  23. package/cjs/interceptors/interceptorManager.cjs +25 -18
  24. package/cjs/interceptors/interceptorManager.cjs.map +1 -1
  25. package/index.d.ts +82 -21
  26. package/package.json +1 -1
  27. package/src/accessio.ts +148 -113
  28. package/src/core/accessioError.ts +57 -1
  29. package/src/core/buildURL.ts +14 -4
  30. package/src/core/fetchAdapter.ts +155 -125
  31. package/src/core/request.ts +85 -27
  32. package/src/core/retry.ts +8 -5
  33. package/src/helpers/debug.ts +7 -2
  34. package/src/helpers/flattenHeaders.ts +4 -1
  35. package/src/helpers/rateLimiter.ts +35 -3
  36. package/src/helpers/settle.ts +1 -1
  37. package/src/helpers/toFormData.ts +5 -1
  38. package/src/interceptors/interceptorManager.ts +26 -19
  39. package/src/types.ts +2 -1
package/src/accessio.ts CHANGED
@@ -15,6 +15,66 @@ import type {
15
15
  } from './types';
16
16
  import defaultsConfig from './defaults/index';
17
17
 
18
+ function runRequestInterceptorsSync(
19
+ startConfig: AccessioRequestConfig,
20
+ interceptors: InterceptorHandler[],
21
+ ): Promise<AccessioRequestConfig> {
22
+ let cfg = startConfig;
23
+ let rejectReason: any = null;
24
+ let isRejected = false;
25
+
26
+ for (const interceptor of interceptors) {
27
+ if (!isRejected) {
28
+ try {
29
+ if (interceptor.fulfilled) {
30
+ cfg = (interceptor.fulfilled as any)(cfg) as AccessioRequestConfig;
31
+ }
32
+ } catch (err) {
33
+ rejectReason = err;
34
+ isRejected = true;
35
+ }
36
+ } else if (interceptor.rejected) {
37
+ try {
38
+ cfg = interceptor.rejected(rejectReason) as AccessioRequestConfig;
39
+ isRejected = false;
40
+ } catch (err) {
41
+ rejectReason = err;
42
+ isRejected = true;
43
+ }
44
+ }
45
+ }
46
+
47
+ return isRejected ? Promise.reject(rejectReason) : Promise.resolve(cfg);
48
+ }
49
+
50
+ function runRequestInterceptorsAsync(
51
+ startConfig: AccessioRequestConfig,
52
+ interceptors: InterceptorHandler[],
53
+ ): Promise<AccessioRequestConfig> {
54
+ let promise: Promise<any> = Promise.resolve(startConfig);
55
+ for (const interceptor of interceptors) {
56
+ promise = promise.then(
57
+ (value: any) => (interceptor.fulfilled ? (interceptor.fulfilled as any)(value) : value),
58
+ interceptor.rejected ?? undefined,
59
+ );
60
+ }
61
+ return promise as Promise<AccessioRequestConfig>;
62
+ }
63
+
64
+ function dispatchAndRetry(cfg: AccessioRequestConfig): Promise<AccessioResponse> {
65
+ const fullUrl = buildURL(cfg.url ?? '', cfg.baseURL, cfg.params, cfg.paramsSerializer);
66
+ logRequest(cfg, fullUrl);
67
+
68
+ const enrichedCfg = fullUrl !== (cfg.url || '') ? { ...cfg, _builtUrl: fullUrl } : cfg;
69
+
70
+ const dispatchFn = cfg.rateLimiter
71
+ ? (config: AccessioRequestConfig) =>
72
+ rateLimitedRequest(dispatchRequest, config.rateLimiter!, config)
73
+ : dispatchRequest;
74
+
75
+ return retryRequest(dispatchFn, enrichedCfg);
76
+ }
77
+
18
78
  export class Accessio {
19
79
  defaults: AccessioRequestConfig;
20
80
  interceptors: Interceptors;
@@ -51,86 +111,17 @@ export class Accessio {
51
111
  );
52
112
  }
53
113
 
54
- const requestInterceptors: any[] = [];
55
- const responseInterceptors: any[] = [];
56
- let synchronousRequestInterceptors = true;
57
-
58
- this.interceptors.request.forEach((interceptor: InterceptorHandler) => {
59
- if (interceptor.runWhen && !interceptor.runWhen(mergedConfig)) {
60
- return;
61
- }
62
- synchronousRequestInterceptors = synchronousRequestInterceptors && interceptor.synchronous;
63
- requestInterceptors.unshift(interceptor);
64
- });
65
-
66
- this.interceptors.response.forEach((interceptor: InterceptorHandler) => {
67
- responseInterceptors.push(interceptor);
68
- });
69
-
70
- let promise: Promise<any>;
71
-
72
- if (synchronousRequestInterceptors) {
73
- let newConfig = mergedConfig;
74
- let rejectReason: any = null;
75
- let isRejected = false;
76
-
77
- for (const interceptor of requestInterceptors) {
78
- if (!isRejected) {
79
- try {
80
- if (interceptor.fulfilled) {
81
- newConfig = interceptor.fulfilled(newConfig);
82
- }
83
- } catch (err) {
84
- rejectReason = err;
85
- isRejected = true;
86
- }
87
- } else {
88
- if (interceptor.rejected) {
89
- try {
90
- newConfig = interceptor.rejected(rejectReason);
91
- isRejected = false;
92
- } catch (err) {
93
- rejectReason = err;
94
- isRejected = true;
95
- }
96
- }
97
- }
98
- }
99
-
100
- if (isRejected) {
101
- promise = Promise.reject(rejectReason);
102
- } else {
103
- promise = Promise.resolve(newConfig);
104
- }
105
- } else {
106
- promise = Promise.resolve(mergedConfig);
107
- for (const interceptor of requestInterceptors) {
108
- promise = promise.then((value: any) => {
109
- if (interceptor.fulfilled) {
110
- return interceptor.fulfilled(value);
111
- }
112
- return value;
113
- }, interceptor.rejected);
114
- }
115
- }
116
-
117
- promise = promise.then((cfg: any) => {
118
- const fullUrl = buildURL(cfg.url ?? '', cfg.baseURL, cfg.params, cfg.paramsSerializer);
119
-
120
- logRequest(cfg, fullUrl);
114
+ const { requestInterceptors, responseInterceptors, synchronous } =
115
+ this.collectInterceptors(mergedConfig);
121
116
 
122
- const enrichedCfg = fullUrl !== (cfg.url || '') ? { ...cfg, _builtUrl: fullUrl } : cfg;
117
+ let promise: Promise<any> = synchronous
118
+ ? runRequestInterceptorsSync(mergedConfig, requestInterceptors)
119
+ : runRequestInterceptorsAsync(mergedConfig, requestInterceptors);
123
120
 
124
- const dispatchFn = cfg.rateLimiter
125
- ? (config: AccessioRequestConfig) =>
126
- rateLimitedRequest(dispatchRequest, config.rateLimiter!, config)
127
- : dispatchRequest;
128
-
129
- return retryRequest(dispatchFn, enrichedCfg);
130
- });
121
+ promise = promise.then((cfg: AccessioRequestConfig) => dispatchAndRetry(cfg));
131
122
 
132
123
  promise = promise.then(
133
- (value: any) => {
124
+ (value: AccessioResponse) => {
134
125
  logResponse(value);
135
126
  return value;
136
127
  },
@@ -143,15 +134,37 @@ export class Accessio {
143
134
  for (const interceptor of responseInterceptors) {
144
135
  promise = promise.then((value: any) => {
145
136
  if (interceptor.fulfilled) {
146
- return interceptor.fulfilled(value);
137
+ return (interceptor.fulfilled as any)(value);
147
138
  }
148
139
  return value;
149
- }, interceptor.rejected);
140
+ }, interceptor.rejected ?? undefined);
150
141
  }
151
142
 
152
143
  return promise;
153
144
  }
154
145
 
146
+ private collectInterceptors(mergedConfig: AccessioRequestConfig): {
147
+ requestInterceptors: InterceptorHandler[];
148
+ responseInterceptors: InterceptorHandler[];
149
+ synchronous: boolean;
150
+ } {
151
+ const requestInterceptors: InterceptorHandler[] = [];
152
+ const responseInterceptors: InterceptorHandler[] = [];
153
+ let synchronous = true;
154
+
155
+ this.interceptors.request.forEach((interceptor: InterceptorHandler) => {
156
+ if (interceptor.runWhen && !interceptor.runWhen(mergedConfig)) return;
157
+ synchronous = synchronous && interceptor.synchronous;
158
+ requestInterceptors.unshift(interceptor);
159
+ });
160
+
161
+ this.interceptors.response.forEach((interceptor: InterceptorHandler) => {
162
+ responseInterceptors.push(interceptor);
163
+ });
164
+
165
+ return { requestInterceptors, responseInterceptors, synchronous };
166
+ }
167
+
155
168
  getUri(config?: AccessioRequestConfig): string {
156
169
  const merged = mergeConfig(this.defaults, config);
157
170
  return buildURL(merged.url ?? '', merged.baseURL, merged.params, merged.paramsSerializer);
@@ -197,7 +210,8 @@ export class Accessio {
197
210
  return this.request<T>(mergeConfig(config || {}, { method: 'patch', url, data }));
198
211
  }
199
212
 
200
- postForm<T = any>(
213
+ private formRequest<T = any>(
214
+ method: 'post' | 'put' | 'patch',
201
215
  url: string,
202
216
  data?: any,
203
217
  config?: AccessioRequestConfig,
@@ -205,7 +219,7 @@ export class Accessio {
205
219
  const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
206
220
  return this.request<T>(
207
221
  mergeConfig(config || {}, {
208
- method: 'post',
222
+ method,
209
223
  url,
210
224
  data: formData,
211
225
  headers: { 'Content-Type': 'multipart/form-data' },
@@ -213,20 +227,20 @@ export class Accessio {
213
227
  );
214
228
  }
215
229
 
230
+ postForm<T = any>(
231
+ url: string,
232
+ data?: any,
233
+ config?: AccessioRequestConfig,
234
+ ): Promise<AccessioResponse<T>> {
235
+ return this.formRequest<T>('post', url, data, config);
236
+ }
237
+
216
238
  putForm<T = any>(
217
239
  url: string,
218
240
  data?: any,
219
241
  config?: AccessioRequestConfig,
220
242
  ): Promise<AccessioResponse<T>> {
221
- const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
222
- return this.request<T>(
223
- mergeConfig(config || {}, {
224
- method: 'put',
225
- url,
226
- data: formData,
227
- headers: { 'Content-Type': 'multipart/form-data' },
228
- }),
229
- );
243
+ return this.formRequest<T>('put', url, data, config);
230
244
  }
231
245
 
232
246
  patchForm<T = any>(
@@ -234,15 +248,7 @@ export class Accessio {
234
248
  data?: any,
235
249
  config?: AccessioRequestConfig,
236
250
  ): Promise<AccessioResponse<T>> {
237
- const formData = data && !(data instanceof FormData) ? toFormData(data) : data;
238
- return this.request<T>(
239
- mergeConfig(config || {}, {
240
- method: 'patch',
241
- url,
242
- data: formData,
243
- headers: { 'Content-Type': 'multipart/form-data' },
244
- }),
245
- );
251
+ return this.formRequest<T>('patch', url, data, config);
246
252
  }
247
253
 
248
254
  async *stream<T = any>(
@@ -255,19 +261,13 @@ export class Accessio {
255
261
  if (!response.data) return;
256
262
 
257
263
  const reader = response.data.getReader();
258
- const decoder = new TextDecoder();
259
- let buffer = '';
264
+ try {
265
+ const decoder = new TextDecoder();
266
+ let buffer = '';
260
267
 
261
- while (true) {
262
- const { done, value } = await reader.read();
263
- if (done) break;
264
-
265
- buffer += decoder.decode(value, { stream: true });
266
- const lines = buffer.split('\n');
267
- buffer = lines.pop() || '';
268
-
269
- for (const line of lines) {
270
- if (line.trim().startsWith('data:')) {
268
+ const processLine = function* (line: string) {
269
+ const trimmed = line.trim();
270
+ if (trimmed.startsWith('data:')) {
271
271
  const dataStr = line.replace(/^data:\s*/, '');
272
272
  if (dataStr === '[DONE]') return;
273
273
  try {
@@ -275,14 +275,39 @@ export class Accessio {
275
275
  } catch (e) {
276
276
  yield dataStr as any;
277
277
  }
278
- } else if (line.trim().startsWith('{') || line.trim().startsWith('[')) {
278
+ } else if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
279
279
  try {
280
280
  yield JSON.parse(line);
281
281
  } catch (e) {
282
282
  // ignore partial json
283
283
  }
284
284
  }
285
+ };
286
+
287
+ while (true) {
288
+ const { done, value } = await reader.read();
289
+ if (done) break;
290
+
291
+ buffer += decoder.decode(value, { stream: true });
292
+ const lines = buffer.split('\n');
293
+ buffer = lines.pop() || '';
294
+
295
+ for (const line of lines) {
296
+ yield* processLine(line);
297
+ }
285
298
  }
299
+
300
+ buffer += decoder.decode(new Uint8Array(), { stream: false });
301
+ if (buffer.trim()) {
302
+ yield* processLine(buffer);
303
+ }
304
+ } finally {
305
+ try {
306
+ await reader.cancel();
307
+ } catch {
308
+ // ignore errors on cancel
309
+ }
310
+ reader.releaseLock();
286
311
  }
287
312
  }
288
313
 
@@ -296,14 +321,24 @@ export class Accessio {
296
321
  while (nextUrl) {
297
322
  const response: AccessioResponse<any> = await this.get(nextUrl, currentConfig);
298
323
 
299
- const items = Array.isArray(response.data) ? response.data : response.data.data;
324
+ const data = response.data;
325
+ const items = Array.isArray(data)
326
+ ? data
327
+ : data && typeof data === 'object'
328
+ ? (data as any).data
329
+ : null;
330
+
300
331
  if (Array.isArray(items)) {
301
332
  for (const item of items) {
302
333
  yield item;
303
334
  }
304
335
  }
305
336
 
306
- nextUrl = response.data.next || response.data.links?.next || null;
337
+ nextUrl =
338
+ data && typeof data === 'object'
339
+ ? (data as any).next || (data as any).links?.next || null
340
+ : null;
341
+
307
342
  if (nextUrl) {
308
343
  currentConfig = mergeConfig(currentConfig, { url: nextUrl, params: {} });
309
344
  }
@@ -6,7 +6,7 @@ function redactHeaders(headers: unknown): unknown {
6
6
  const out: Record<string, unknown> = {};
7
7
  for (const key of Object.keys(headers as Record<string, unknown>)) {
8
8
  const value = (headers as Record<string, unknown>)[key];
9
- if (/^authorization$/i.test(key)) {
9
+ if (/^authorization$/i.test(key) || /^cookie$/i.test(key) || /^set-cookie$/i.test(key)) {
10
10
  out[key] = '[REDACTED]';
11
11
  } else if (value && typeof value === 'object' && !Array.isArray(value)) {
12
12
  out[key] = redactHeaders(value);
@@ -17,11 +17,67 @@ function redactHeaders(headers: unknown): unknown {
17
17
  return out;
18
18
  }
19
19
 
20
+ const SENSITIVE_BODY_KEY =
21
+ /^(password|passwd|pwd|token|access_token|refresh_token|id_token|authorization|api[_-]?key|secret|client[_-]?secret|cookie|set[_-]?cookie|private[_-]?key|session)$/i;
22
+
23
+ export function redactBody(value: unknown, seen?: WeakSet<object>): unknown {
24
+ if (value === null || typeof value !== 'object') return value;
25
+ const visited = seen ?? new WeakSet<object>();
26
+ if (visited.has(value as object)) return '[Circular]';
27
+ visited.add(value as object);
28
+
29
+ if (Array.isArray(value)) {
30
+ return value.map((item) => redactBody(item, visited));
31
+ }
32
+
33
+ const out: Record<string, unknown> = {};
34
+ for (const key of Object.keys(value as Record<string, unknown>)) {
35
+ const v = (value as Record<string, unknown>)[key];
36
+ if (SENSITIVE_BODY_KEY.test(key)) {
37
+ out[key] = '[REDACTED]';
38
+ } else {
39
+ out[key] = redactBody(v, visited);
40
+ }
41
+ }
42
+ return out;
43
+ }
44
+
45
+ function redactParams(params: unknown): unknown {
46
+ if (!params || typeof params !== 'object') return params;
47
+ const out: Record<string, unknown> = {};
48
+ for (const key of Object.keys(params as Record<string, unknown>)) {
49
+ const value = (params as Record<string, unknown>)[key];
50
+ if (SENSITIVE_BODY_KEY.test(key)) {
51
+ out[key] = '[REDACTED]';
52
+ } else if (value && typeof value === 'object' && !Array.isArray(value)) {
53
+ out[key] = redactParams(value);
54
+ } else {
55
+ out[key] = value;
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ function redactURL(url: string | undefined): string | undefined {
62
+ if (!url) return url;
63
+ // Match inline credentials: http://user:pass@host
64
+ return url.replace(/^([a-z][a-z\d+\-.]*:\/\/)([^/]+)@/i, (match, protocol, userInfo) => {
65
+ const parts = userInfo.split(':');
66
+ if (parts.length > 1) {
67
+ return `${protocol}${parts[0]}:[REDACTED]@`;
68
+ }
69
+ return `${protocol}[REDACTED]@`;
70
+ });
71
+ }
72
+
20
73
  export function redactConfig(config: AccessioRequestConfig | null): AccessioRequestConfig | null {
21
74
  if (!config) return config;
22
75
  const clone = { ...config } as AccessioRequestConfig & { auth?: unknown };
23
76
  if ('auth' in clone) delete clone.auth;
24
77
  if (clone.headers) clone.headers = redactHeaders(clone.headers) as typeof clone.headers;
78
+ if (clone.params) clone.params = redactParams(clone.params) as typeof clone.params;
79
+ if (clone.url) clone.url = redactURL(clone.url);
80
+ if (clone._builtUrl) clone._builtUrl = redactURL(clone._builtUrl);
25
81
  return clone;
26
82
  }
27
83
 
@@ -76,11 +76,19 @@ export default function buildURL(
76
76
  let fullURL = baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url || '';
77
77
 
78
78
  let finalParams = params;
79
- if (params && typeof params === 'object') {
80
- const unusedParams = { ...params };
79
+ if (params && typeof params === 'object' && !(params instanceof URLSearchParams)) {
80
+ const unusedParams: Record<string, unknown> = {};
81
+ for (const key of Object.keys(params)) {
82
+ if (key === '__proto__' || key === 'prototype' || key === 'constructor') continue;
83
+ unusedParams[key] = (params as Record<string, unknown>)[key];
84
+ }
81
85
  fullURL = fullURL.replace(/(?::([a-zA-Z0-9_]+))|(?:{([a-zA-Z0-9_]+)})/g, (match, p1, p2) => {
82
86
  const key = p1 || p2;
83
- if (key && unusedParams[key] !== undefined) {
87
+ if (
88
+ key &&
89
+ Object.prototype.hasOwnProperty.call(unusedParams, key) &&
90
+ unusedParams[key] !== undefined
91
+ ) {
84
92
  const val = unusedParams[key];
85
93
  delete unusedParams[key];
86
94
  return encodeURIComponent(String(val));
@@ -93,10 +101,12 @@ export default function buildURL(
93
101
  const serialized = serializeParams(finalParams as Record<string, unknown>, paramsSerializer);
94
102
  if (serialized) {
95
103
  const hashIndex = fullURL.indexOf('#');
104
+ let fragment = '';
96
105
  if (hashIndex !== -1) {
106
+ fragment = fullURL.slice(hashIndex);
97
107
  fullURL = fullURL.slice(0, hashIndex);
98
108
  }
99
- fullURL += (fullURL.indexOf('?') === -1 ? '?' : '&') + serialized;
109
+ fullURL += (fullURL.indexOf('?') === -1 ? '?' : '&') + serialized + fragment;
100
110
  }
101
111
 
102
112
  return fullURL;