hanzio 1.0.9 → 1.1.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/dist/api-wrapper/actions-impl.d.ts +4 -0
- package/dist/api-wrapper/builders.d.ts +12 -0
- package/dist/api-wrapper/create-client.d.ts +6 -0
- package/dist/api-wrapper/error-reporting.d.ts +7 -0
- package/dist/api-wrapper/errors.d.ts +63 -0
- package/dist/api-wrapper/headers.d.ts +9 -0
- package/dist/api-wrapper/index.d.ts +5 -84
- package/dist/api-wrapper/redaction.d.ts +2 -0
- package/dist/api-wrapper/shared.d.ts +16 -0
- package/dist/api-wrapper/size.d.ts +1 -0
- package/dist/api-wrapper/test-helpers.d.ts +6 -0
- package/dist/api-wrapper/transport.d.ts +52 -0
- package/dist/api-wrapper/types.d.ts +153 -0
- package/dist/api-wrapper/zod-issues.d.ts +17 -0
- package/dist/secrets/index.d.ts +10 -8
- package/dist/secrets/infisical.d.ts +19 -2
- package/dist/secrets/loaders.d.ts +16 -0
- package/dist/src/api-wrapper/index.js +1018 -166
- package/dist/src/index.js +1155 -232
- package/dist/src/secrets/index.js +1138 -230
- package/dist/src/secrets/infisical.js +1028 -173
- package/dist/src/secrets/loaders.js +1236 -0
- package/package.json +2 -2
- package/src/api-wrapper/actions-impl.ts +100 -0
- package/src/api-wrapper/builders.ts +175 -0
- package/src/api-wrapper/create-client.ts +373 -0
- package/src/api-wrapper/error-reporting.ts +31 -0
- package/src/api-wrapper/errors.ts +221 -0
- package/src/api-wrapper/headers.ts +66 -0
- package/src/api-wrapper/index.ts +5 -492
- package/src/api-wrapper/redaction.ts +61 -0
- package/src/api-wrapper/shared.ts +25 -0
- package/src/api-wrapper/size.ts +19 -0
- package/src/api-wrapper/test-helpers.ts +25 -0
- package/src/api-wrapper/transport.ts +354 -0
- package/src/api-wrapper/types.ts +258 -0
- package/src/api-wrapper/zod-issues.ts +157 -0
- package/src/secrets/index.ts +125 -81
- package/src/secrets/infisical.ts +50 -14
- package/src/secrets/loaders.ts +67 -0
|
@@ -1,156 +1,350 @@
|
|
|
1
|
-
// src/api-wrapper/
|
|
1
|
+
// src/api-wrapper/headers.ts
|
|
2
|
+
var CANONICAL_HEADER_NAMES = {
|
|
3
|
+
"content-type": "Content-Type",
|
|
4
|
+
authorization: "Authorization",
|
|
5
|
+
accept: "Accept",
|
|
6
|
+
"user-agent": "User-Agent",
|
|
7
|
+
"x-request-id": "X-Request-Id",
|
|
8
|
+
"x-correlation-id": "X-Correlation-Id"
|
|
9
|
+
};
|
|
10
|
+
function normalizeHeaders(headers) {
|
|
11
|
+
const result = {};
|
|
12
|
+
const lowerToCanonical = new Map;
|
|
13
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
14
|
+
const lower = key.toLowerCase();
|
|
15
|
+
const canonical = CANONICAL_HEADER_NAMES[lower] ?? key;
|
|
16
|
+
const existing = lowerToCanonical.get(lower);
|
|
17
|
+
if (existing && existing !== canonical)
|
|
18
|
+
delete result[existing];
|
|
19
|
+
lowerToCanonical.set(lower, canonical);
|
|
20
|
+
result[canonical] = value;
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
function getHeader(headers, name) {
|
|
25
|
+
const lower = name.toLowerCase();
|
|
26
|
+
const canonical = CANONICAL_HEADER_NAMES[lower];
|
|
27
|
+
if (canonical !== undefined) {
|
|
28
|
+
if (headers[canonical] !== undefined)
|
|
29
|
+
return headers[canonical];
|
|
30
|
+
}
|
|
31
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
32
|
+
if (k.toLowerCase() === lower)
|
|
33
|
+
return v;
|
|
34
|
+
}
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
function hasJsonContentType(headers) {
|
|
38
|
+
const ct = getHeader(headers, "content-type") ?? "";
|
|
39
|
+
return ct.includes("application/json");
|
|
40
|
+
}
|
|
41
|
+
function buildFetchBody(body, headers) {
|
|
42
|
+
if (body === undefined || body === null)
|
|
43
|
+
return;
|
|
44
|
+
if (body instanceof FormData || body instanceof Blob)
|
|
45
|
+
return body;
|
|
46
|
+
if (hasJsonContentType(headers))
|
|
47
|
+
return JSON.stringify(body);
|
|
48
|
+
return body;
|
|
49
|
+
}
|
|
50
|
+
function getHeadersAsObject(headers) {
|
|
51
|
+
const result = {};
|
|
52
|
+
headers.forEach((value, key) => {
|
|
53
|
+
result[key] = value;
|
|
54
|
+
});
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// src/api-wrapper/zod-issues.ts
|
|
2
59
|
import { ZodError } from "zod";
|
|
60
|
+
function formatZodIssues(error, rawData) {
|
|
61
|
+
return error.issues.map((issue) => {
|
|
62
|
+
const pathArray = [...issue.path];
|
|
63
|
+
const path = formatIssuePath(pathArray);
|
|
64
|
+
const value = getValueAtPath(rawData, pathArray);
|
|
65
|
+
const valuePreview = value === undefined ? undefined : previewValue(value);
|
|
66
|
+
const raw = issue;
|
|
67
|
+
const expected = typeof raw.expected === "string" ? raw.expected : undefined;
|
|
68
|
+
const received = typeof raw.received === "string" ? raw.received : value !== undefined ? typeofVerbose(value) : undefined;
|
|
69
|
+
const known = new Set([
|
|
70
|
+
"code",
|
|
71
|
+
"path",
|
|
72
|
+
"message",
|
|
73
|
+
"expected",
|
|
74
|
+
"received",
|
|
75
|
+
"fatal"
|
|
76
|
+
]);
|
|
77
|
+
const extra = {};
|
|
78
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
79
|
+
if (!known.has(k) && v !== undefined)
|
|
80
|
+
extra[k] = v;
|
|
81
|
+
}
|
|
82
|
+
return {
|
|
83
|
+
path,
|
|
84
|
+
pathArray,
|
|
85
|
+
code: issue.code,
|
|
86
|
+
message: issue.message,
|
|
87
|
+
expected,
|
|
88
|
+
received,
|
|
89
|
+
value,
|
|
90
|
+
valuePreview,
|
|
91
|
+
extra: Object.keys(extra).length > 0 ? extra : undefined
|
|
92
|
+
};
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function renderIssueSummary(issues) {
|
|
96
|
+
if (issues.length === 0)
|
|
97
|
+
return "(no issues reported)";
|
|
98
|
+
return issues.map((issue, idx) => {
|
|
99
|
+
const parts = [];
|
|
100
|
+
parts.push(` [${idx + 1}] ${issue.path}: ${issue.message}`);
|
|
101
|
+
if (issue.expected || issue.received) {
|
|
102
|
+
const exp = issue.expected ? `expected ${issue.expected}` : "";
|
|
103
|
+
const rec = issue.received ? `received ${issue.received}` : "";
|
|
104
|
+
parts.push(` (${[exp, rec].filter(Boolean).join(", ")})`);
|
|
105
|
+
}
|
|
106
|
+
if (issue.valuePreview !== undefined) {
|
|
107
|
+
parts.push(` value: ${issue.valuePreview}`);
|
|
108
|
+
}
|
|
109
|
+
if (issue.extra) {
|
|
110
|
+
for (const [k, v] of Object.entries(issue.extra)) {
|
|
111
|
+
parts.push(` ${k}: ${previewValue(v, 120)}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return parts.join(`
|
|
115
|
+
`);
|
|
116
|
+
}).join(`
|
|
117
|
+
`);
|
|
118
|
+
}
|
|
119
|
+
function formatIssuePath(pathArray) {
|
|
120
|
+
if (pathArray.length === 0)
|
|
121
|
+
return "<root>";
|
|
122
|
+
let out = "";
|
|
123
|
+
for (const segment of pathArray) {
|
|
124
|
+
if (typeof segment === "number") {
|
|
125
|
+
out += `[${segment}]`;
|
|
126
|
+
} else if (/^[A-Za-z_$][\w$]*$/.test(segment)) {
|
|
127
|
+
out += out === "" ? segment : `.${segment}`;
|
|
128
|
+
} else {
|
|
129
|
+
out += `[${JSON.stringify(segment)}]`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
function getValueAtPath(data, pathArray) {
|
|
135
|
+
let current = data;
|
|
136
|
+
for (const segment of pathArray) {
|
|
137
|
+
if (current === null || current === undefined)
|
|
138
|
+
return;
|
|
139
|
+
if (typeof current !== "object")
|
|
140
|
+
return;
|
|
141
|
+
current = current[segment];
|
|
142
|
+
}
|
|
143
|
+
return current;
|
|
144
|
+
}
|
|
145
|
+
function typeofVerbose(value) {
|
|
146
|
+
if (value === null)
|
|
147
|
+
return "null";
|
|
148
|
+
if (Array.isArray(value))
|
|
149
|
+
return "array";
|
|
150
|
+
return typeof value;
|
|
151
|
+
}
|
|
152
|
+
function previewValue(value, maxLen = 200) {
|
|
153
|
+
let str;
|
|
154
|
+
try {
|
|
155
|
+
if (value === undefined)
|
|
156
|
+
str = "undefined";
|
|
157
|
+
else if (typeof value === "string")
|
|
158
|
+
str = JSON.stringify(value);
|
|
159
|
+
else
|
|
160
|
+
str = JSON.stringify(value);
|
|
161
|
+
} catch {
|
|
162
|
+
str = String(value);
|
|
163
|
+
}
|
|
164
|
+
if (str === undefined)
|
|
165
|
+
str = String(value);
|
|
166
|
+
if (str.length > maxLen)
|
|
167
|
+
str = `${str.slice(0, maxLen)}…`;
|
|
168
|
+
return str;
|
|
169
|
+
}
|
|
170
|
+
function makeJsonParseZodError(error, text) {
|
|
171
|
+
const message = error instanceof Error ? `Response body was not valid JSON: ${error.message}` : "Response body was not valid JSON";
|
|
172
|
+
const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
|
|
173
|
+
return new ZodError([
|
|
174
|
+
{
|
|
175
|
+
code: "custom",
|
|
176
|
+
path: [],
|
|
177
|
+
message: `${message} (body preview: ${JSON.stringify(preview)})`,
|
|
178
|
+
input: text
|
|
179
|
+
}
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
function tryParseJson(body) {
|
|
183
|
+
if (!body)
|
|
184
|
+
return;
|
|
185
|
+
try {
|
|
186
|
+
return JSON.parse(body);
|
|
187
|
+
} catch {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
3
191
|
|
|
4
|
-
|
|
192
|
+
// src/api-wrapper/errors.ts
|
|
193
|
+
var MAX_BODY_PREVIEW = 500;
|
|
194
|
+
|
|
195
|
+
class ApiError extends Error {
|
|
196
|
+
context;
|
|
197
|
+
cause;
|
|
198
|
+
constructor(message, context, options) {
|
|
199
|
+
super(message);
|
|
200
|
+
this.name = "ApiError";
|
|
201
|
+
this.context = context;
|
|
202
|
+
this.cause = options?.cause;
|
|
203
|
+
}
|
|
204
|
+
toJSON() {
|
|
205
|
+
return {
|
|
206
|
+
name: this.name,
|
|
207
|
+
message: this.message,
|
|
208
|
+
context: this.context
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
class HttpResponseError extends ApiError {
|
|
214
|
+
status;
|
|
215
|
+
body;
|
|
216
|
+
bodyJson;
|
|
217
|
+
responseHeaders;
|
|
218
|
+
requestId;
|
|
219
|
+
constructor(status, body, responseHeaders, context) {
|
|
220
|
+
const preview = body.length > MAX_BODY_PREVIEW ? `${body.slice(0, MAX_BODY_PREVIEW)}…` : body;
|
|
221
|
+
super(`HTTP ${status} ${context.method} ${context.url} - ${preview}`, context);
|
|
222
|
+
this.name = "HttpResponseError";
|
|
223
|
+
this.status = status;
|
|
224
|
+
this.body = body;
|
|
225
|
+
this.responseHeaders = responseHeaders;
|
|
226
|
+
this.requestId = getHeader(responseHeaders, "x-request-id") ?? getHeader(responseHeaders, "x-correlation-id");
|
|
227
|
+
this.bodyJson = tryParseJson(body);
|
|
228
|
+
}
|
|
229
|
+
toJSON() {
|
|
230
|
+
return {
|
|
231
|
+
...super.toJSON(),
|
|
232
|
+
status: this.status,
|
|
233
|
+
body: this.body,
|
|
234
|
+
bodyJson: this.bodyJson,
|
|
235
|
+
requestId: this.requestId,
|
|
236
|
+
responseHeaders: this.responseHeaders
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
class ResponseValidationError extends ApiError {
|
|
5
242
|
zodError;
|
|
6
243
|
rawResponse;
|
|
244
|
+
issues;
|
|
7
245
|
validationIssues;
|
|
8
|
-
constructor(zodError, rawResponse) {
|
|
9
|
-
const issues = zodError
|
|
10
|
-
|
|
246
|
+
constructor(zodError, rawResponse, context) {
|
|
247
|
+
const issues = formatZodIssues(zodError, rawResponse);
|
|
248
|
+
const summary = renderIssueSummary(issues);
|
|
249
|
+
super(`Response validation failed (${context.method} ${context.url}):
|
|
250
|
+
${summary}`, context, { cause: zodError });
|
|
11
251
|
this.name = "ResponseValidationError";
|
|
12
252
|
this.zodError = zodError;
|
|
13
253
|
this.rawResponse = rawResponse;
|
|
14
|
-
this.
|
|
254
|
+
this.issues = issues;
|
|
255
|
+
this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
256
|
+
}
|
|
257
|
+
toJSON() {
|
|
258
|
+
return {
|
|
259
|
+
...super.toJSON(),
|
|
260
|
+
issues: this.issues,
|
|
261
|
+
validationIssues: this.validationIssues,
|
|
262
|
+
rawResponsePreview: previewValue(this.rawResponse, 1000)
|
|
263
|
+
};
|
|
15
264
|
}
|
|
16
265
|
}
|
|
17
266
|
|
|
18
|
-
class
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
267
|
+
class RequestValidationError extends ApiError {
|
|
268
|
+
zodError;
|
|
269
|
+
target;
|
|
270
|
+
rawInput;
|
|
271
|
+
issues;
|
|
272
|
+
validationIssues;
|
|
273
|
+
constructor(zodError, target, rawInput, context) {
|
|
274
|
+
const issues = formatZodIssues(zodError, rawInput);
|
|
275
|
+
const summary = renderIssueSummary(issues);
|
|
276
|
+
super(`Request ${target} validation failed (${context.method} ${context.url}):
|
|
277
|
+
${summary}`, context, { cause: zodError });
|
|
278
|
+
this.name = "RequestValidationError";
|
|
279
|
+
this.zodError = zodError;
|
|
280
|
+
this.target = target;
|
|
281
|
+
this.rawInput = rawInput;
|
|
282
|
+
this.issues = issues;
|
|
283
|
+
this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
|
|
284
|
+
}
|
|
285
|
+
toJSON() {
|
|
286
|
+
return {
|
|
287
|
+
...super.toJSON(),
|
|
288
|
+
target: this.target,
|
|
289
|
+
issues: this.issues,
|
|
290
|
+
validationIssues: this.validationIssues
|
|
291
|
+
};
|
|
26
292
|
}
|
|
27
293
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const startedAt = Date.now();
|
|
37
|
-
let retryCount = 0;
|
|
38
|
-
let httpStatus = 0;
|
|
39
|
-
apiConfig.onRequestStart?.({ endpoint: endpointName, url: fullUrl });
|
|
40
|
-
try {
|
|
41
|
-
const config = {
|
|
42
|
-
method: endpoint.method,
|
|
43
|
-
url: finalUrl,
|
|
44
|
-
queryParams: buildQueryParams(endpoint, input.reqQuery),
|
|
45
|
-
body: buildRequestBody(endpoint, input.reqBody),
|
|
46
|
-
headers: buildHeaders(endpoint, input.reqHeaders, apiConfig.defaultHeaders ?? {}),
|
|
47
|
-
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
48
|
-
timeoutMs: apiConfig.timeoutMs,
|
|
49
|
-
fetchFn: apiConfig.fetch ?? fetch
|
|
50
|
-
};
|
|
51
|
-
const result = await makeRequestWithRetry(config, apiConfig.retries ?? 3, apiConfig.retryDelayMs ?? 300, (response) => validateAndTransformResponse(endpoint, response), apiConfig.shouldRetry ?? defaultShouldRetry, apiConfig.logger);
|
|
52
|
-
retryCount = result.retryCount;
|
|
53
|
-
httpStatus = result.httpStatus;
|
|
54
|
-
const responseTimeMs = Date.now() - startedAt;
|
|
55
|
-
const responseSizeMb = calculateSizeInMb(result.validatedData);
|
|
56
|
-
const requestSizeMb = calculateSizeInMb({
|
|
57
|
-
method: config.method,
|
|
58
|
-
url: fullUrl,
|
|
59
|
-
body: config.body,
|
|
60
|
-
headers: config.headers
|
|
61
|
-
});
|
|
62
|
-
apiConfig.onRequestEnd?.({
|
|
63
|
-
endpoint: endpointName,
|
|
64
|
-
url: fullUrl,
|
|
65
|
-
status: httpStatus,
|
|
66
|
-
retryCount,
|
|
67
|
-
responseTimeMs
|
|
68
|
-
});
|
|
69
|
-
return {
|
|
70
|
-
data: result.validatedData,
|
|
71
|
-
requestSizeMb,
|
|
72
|
-
responseSizeMb,
|
|
73
|
-
requestSizeTotalMb: requestSizeMb + responseSizeMb,
|
|
74
|
-
responseTimeMs,
|
|
75
|
-
httpStatus,
|
|
76
|
-
retryCount
|
|
77
|
-
};
|
|
78
|
-
} catch (error) {
|
|
79
|
-
apiConfig.logger?.error?.(error);
|
|
80
|
-
throw error;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
return { request };
|
|
294
|
+
|
|
295
|
+
class RequestTimeoutError extends ApiError {
|
|
296
|
+
timeoutMs;
|
|
297
|
+
constructor(timeoutMs, context) {
|
|
298
|
+
super(`Request timed out after ${timeoutMs}ms: ${context.method} ${context.url}`, context);
|
|
299
|
+
this.name = "RequestTimeoutError";
|
|
300
|
+
this.timeoutMs = timeoutMs;
|
|
301
|
+
}
|
|
84
302
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (error instanceof ZodError) {
|
|
91
|
-
throw new ResponseValidationError(error, data);
|
|
92
|
-
}
|
|
93
|
-
throw error;
|
|
303
|
+
|
|
304
|
+
class NetworkError extends ApiError {
|
|
305
|
+
constructor(message, context, cause) {
|
|
306
|
+
super(message, context, { cause });
|
|
307
|
+
this.name = "NetworkError";
|
|
94
308
|
}
|
|
95
309
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
try {
|
|
102
|
-
return await config.fetchFn(fullUrl, {
|
|
103
|
-
method: config.method,
|
|
104
|
-
headers: config.headers,
|
|
105
|
-
body: buildFetchBody(config.body, config.headers),
|
|
106
|
-
signal: abortController.signal
|
|
310
|
+
|
|
311
|
+
class RequestAbortedError extends ApiError {
|
|
312
|
+
constructor(context, cause) {
|
|
313
|
+
super(`Request aborted: ${context.method} ${context.url}`, context, {
|
|
314
|
+
cause
|
|
107
315
|
});
|
|
108
|
-
|
|
109
|
-
if (timeout)
|
|
110
|
-
clearTimeout(timeout);
|
|
316
|
+
this.name = "RequestAbortedError";
|
|
111
317
|
}
|
|
112
318
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const response = await httpRequest(config);
|
|
119
|
-
if (response.status >= 400) {
|
|
120
|
-
if (retryCount < maxRetries && shouldRetry({ response, retryCount, maxRetries })) {
|
|
121
|
-
retryCount++;
|
|
122
|
-
await delay(retryDelayMs);
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
throw new HttpResponseError(response.status, await response.text());
|
|
126
|
-
}
|
|
127
|
-
return {
|
|
128
|
-
validatedData: await validateFn(response),
|
|
129
|
-
retryCount,
|
|
130
|
-
httpStatus: response.status
|
|
131
|
-
};
|
|
132
|
-
} catch (error) {
|
|
133
|
-
if (error instanceof HttpResponseError)
|
|
134
|
-
throw error;
|
|
135
|
-
if (retryCount >= maxRetries || !shouldRetry({ error, retryCount, maxRetries })) {
|
|
136
|
-
throw error;
|
|
137
|
-
}
|
|
138
|
-
retryCount++;
|
|
139
|
-
await delay(retryDelayMs);
|
|
140
|
-
}
|
|
319
|
+
|
|
320
|
+
class ConfigError extends ApiError {
|
|
321
|
+
constructor(message, context) {
|
|
322
|
+
super(message, context);
|
|
323
|
+
this.name = "ConfigError";
|
|
141
324
|
}
|
|
142
325
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
326
|
+
|
|
327
|
+
class ActionError extends ApiError {
|
|
328
|
+
constructor(message, context, cause) {
|
|
329
|
+
super(message, context, { cause });
|
|
330
|
+
this.name = "ActionError";
|
|
331
|
+
}
|
|
149
332
|
}
|
|
150
|
-
|
|
333
|
+
var isApiError = (e) => e instanceof ApiError;
|
|
334
|
+
var isHttpResponseError = (e) => e instanceof HttpResponseError;
|
|
335
|
+
var isResponseValidationError = (e) => e instanceof ResponseValidationError;
|
|
336
|
+
var isRequestValidationError = (e) => e instanceof RequestValidationError;
|
|
337
|
+
var isRequestTimeoutError = (e) => e instanceof RequestTimeoutError;
|
|
338
|
+
var isNetworkError = (e) => e instanceof NetworkError;
|
|
339
|
+
var isRequestAbortedError = (e) => e instanceof RequestAbortedError;
|
|
340
|
+
var isConfigError = (e) => e instanceof ConfigError;
|
|
341
|
+
var isActionError = (e) => e instanceof ActionError;
|
|
342
|
+
// src/api-wrapper/builders.ts
|
|
343
|
+
import { ZodError as ZodError2 } from "zod";
|
|
344
|
+
function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url, configError) {
|
|
151
345
|
if (url) {
|
|
152
|
-
const finalUrl2 = replacePathParams(url, params);
|
|
153
|
-
const queryString2 = new URLSearchParams(
|
|
346
|
+
const finalUrl2 = replacePathParams(url, params, configError);
|
|
347
|
+
const queryString2 = new URLSearchParams(buildQueryParamsRaw(endpoint, reqQuery)).toString();
|
|
154
348
|
return {
|
|
155
349
|
finalUrl: finalUrl2,
|
|
156
350
|
fullUrl: queryString2 ? `${finalUrl2}?${queryString2}` : finalUrl2
|
|
@@ -158,32 +352,57 @@ function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, ur
|
|
|
158
352
|
}
|
|
159
353
|
const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
|
|
160
354
|
if (!defaultBaseUrl) {
|
|
161
|
-
throw
|
|
355
|
+
throw configError("At least one base API URL is required");
|
|
162
356
|
}
|
|
163
357
|
const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
|
|
164
358
|
const configBaseUrl = baseApiUrls[baseUrlKey];
|
|
165
359
|
if (!configBaseUrl) {
|
|
166
|
-
throw
|
|
360
|
+
throw configError(`Unknown base API URL: ${baseUrlKey}`);
|
|
167
361
|
}
|
|
168
362
|
const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
|
|
169
363
|
const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
|
|
170
|
-
const finalUrl = replacePathParams(`${baseUrl}${path}`, params);
|
|
171
|
-
const queryString = new URLSearchParams(
|
|
364
|
+
const finalUrl = replacePathParams(`${baseUrl}${path}`, params, configError);
|
|
365
|
+
const queryString = new URLSearchParams(buildQueryParamsRaw(endpoint, reqQuery)).toString();
|
|
172
366
|
return {
|
|
173
367
|
finalUrl,
|
|
174
368
|
fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
|
|
175
369
|
};
|
|
176
370
|
}
|
|
177
|
-
function
|
|
371
|
+
function buildQueryParamsRaw(endpoint, query) {
|
|
178
372
|
const defaultParams = endpoint.reqDefaultQueryParams ?? {};
|
|
179
|
-
const
|
|
373
|
+
const merged = { ...defaultParams, ...query ?? {} };
|
|
374
|
+
return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
|
|
375
|
+
}
|
|
376
|
+
function buildQueryParams(endpoint, query, ctx) {
|
|
377
|
+
const defaultParams = endpoint.reqDefaultQueryParams ?? {};
|
|
378
|
+
let parsedQuery;
|
|
379
|
+
if (endpoint.reqQuerySchema) {
|
|
380
|
+
try {
|
|
381
|
+
parsedQuery = endpoint.reqQuerySchema.parse(query);
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (error instanceof ZodError2) {
|
|
384
|
+
throw new RequestValidationError(error, "query", query, ctx());
|
|
385
|
+
}
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
} else {
|
|
389
|
+
parsedQuery = query ?? {};
|
|
390
|
+
}
|
|
180
391
|
const merged = { ...defaultParams, ...parsedQuery };
|
|
181
392
|
return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
|
|
182
393
|
}
|
|
183
|
-
function buildRequestBody(endpoint, body) {
|
|
394
|
+
function buildRequestBody(endpoint, body, ctx) {
|
|
184
395
|
if (!endpoint.reqBodySchema)
|
|
185
396
|
return body;
|
|
186
|
-
|
|
397
|
+
let parsedBody;
|
|
398
|
+
try {
|
|
399
|
+
parsedBody = endpoint.reqBodySchema.parse(body);
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error instanceof ZodError2) {
|
|
402
|
+
throw new RequestValidationError(error, "body", body, ctx());
|
|
403
|
+
}
|
|
404
|
+
throw error;
|
|
405
|
+
}
|
|
187
406
|
if (endpoint.reqBodyFormat !== "form-data") {
|
|
188
407
|
return parsedBody;
|
|
189
408
|
}
|
|
@@ -195,56 +414,689 @@ function buildRequestBody(endpoint, body) {
|
|
|
195
414
|
}
|
|
196
415
|
return formData;
|
|
197
416
|
}
|
|
198
|
-
function buildHeaders(endpoint, headers, defaultHeaders) {
|
|
417
|
+
function buildHeaders(endpoint, headers, defaultHeaders, ctx) {
|
|
199
418
|
const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
|
|
200
|
-
|
|
201
|
-
|
|
419
|
+
let parsedHeaders;
|
|
420
|
+
if (endpoint.reqHeadersSchema) {
|
|
421
|
+
try {
|
|
422
|
+
parsedHeaders = endpoint.reqHeadersSchema.parse(headers);
|
|
423
|
+
} catch (error) {
|
|
424
|
+
if (error instanceof ZodError2) {
|
|
425
|
+
throw new RequestValidationError(error, "headers", headers, ctx());
|
|
426
|
+
}
|
|
427
|
+
throw error;
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
parsedHeaders = headers ?? {};
|
|
431
|
+
}
|
|
432
|
+
return normalizeHeaders({
|
|
202
433
|
...defaultHeaders,
|
|
203
434
|
...contentType && { "Content-Type": contentType },
|
|
204
435
|
...endpoint.defaultHeaders,
|
|
205
436
|
...parsedHeaders
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
function buildFetchBody(body, headers) {
|
|
209
|
-
if (body === undefined || body === null)
|
|
210
|
-
return;
|
|
211
|
-
if (body instanceof FormData || body instanceof Blob)
|
|
212
|
-
return body;
|
|
213
|
-
if (headers["Content-Type"] === "application/json")
|
|
214
|
-
return JSON.stringify(body);
|
|
215
|
-
return body;
|
|
437
|
+
});
|
|
216
438
|
}
|
|
217
|
-
function replacePathParams(path, params) {
|
|
439
|
+
function replacePathParams(path, params, configError) {
|
|
218
440
|
return path.replace(/:(\w+)/g, (_, key) => {
|
|
219
441
|
const value = params?.[key];
|
|
220
442
|
if (value === undefined) {
|
|
221
|
-
throw
|
|
443
|
+
throw configError(`Missing required path parameter: ${key}`);
|
|
222
444
|
}
|
|
223
445
|
return encodeURIComponent(String(value));
|
|
224
446
|
});
|
|
225
447
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
448
|
+
|
|
449
|
+
// src/api-wrapper/actions-impl.ts
|
|
450
|
+
function createActionCache() {
|
|
451
|
+
const store = new Map;
|
|
452
|
+
const isExpired = (entry) => entry.expiresAt !== undefined && entry.expiresAt <= Date.now();
|
|
453
|
+
const cache = async (key, fn, options) => {
|
|
454
|
+
const existing = store.get(key);
|
|
455
|
+
if (existing && !isExpired(existing)) {
|
|
456
|
+
if (existing.inflight)
|
|
457
|
+
return existing.inflight;
|
|
458
|
+
if (existing.hasValue)
|
|
459
|
+
return existing.value;
|
|
460
|
+
}
|
|
461
|
+
const promise = (async () => fn())();
|
|
462
|
+
const entry = {
|
|
463
|
+
hasValue: false,
|
|
464
|
+
inflight: promise,
|
|
465
|
+
expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
|
|
466
|
+
};
|
|
467
|
+
store.set(key, entry);
|
|
468
|
+
try {
|
|
469
|
+
const value = await promise;
|
|
470
|
+
entry.value = value;
|
|
471
|
+
entry.hasValue = true;
|
|
472
|
+
entry.inflight = undefined;
|
|
473
|
+
return value;
|
|
474
|
+
} catch (error) {
|
|
475
|
+
store.delete(key);
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
cache.get = (key) => {
|
|
480
|
+
const entry = store.get(key);
|
|
481
|
+
if (!entry || !entry.hasValue)
|
|
482
|
+
return;
|
|
483
|
+
if (isExpired(entry)) {
|
|
484
|
+
store.delete(key);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
return entry.value;
|
|
488
|
+
};
|
|
489
|
+
cache.set = (key, value, options) => {
|
|
490
|
+
store.set(key, {
|
|
491
|
+
value,
|
|
492
|
+
hasValue: true,
|
|
493
|
+
expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
|
|
494
|
+
});
|
|
495
|
+
};
|
|
496
|
+
cache.invalidate = (key) => {
|
|
497
|
+
store.delete(key);
|
|
498
|
+
};
|
|
499
|
+
cache.clear = () => {
|
|
500
|
+
store.clear();
|
|
501
|
+
};
|
|
502
|
+
return cache;
|
|
503
|
+
}
|
|
504
|
+
function makeDefineAction() {
|
|
505
|
+
function defineAction(...args) {
|
|
506
|
+
if (args.length === 0) {
|
|
507
|
+
return (def2) => ({
|
|
508
|
+
...def2,
|
|
509
|
+
noRuntimeInput: false
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
const def = args[0];
|
|
513
|
+
return {
|
|
514
|
+
...def,
|
|
515
|
+
noRuntimeInput: true
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return defineAction;
|
|
519
|
+
}
|
|
520
|
+
var defineAction = makeDefineAction();
|
|
521
|
+
|
|
522
|
+
// src/api-wrapper/redaction.ts
|
|
523
|
+
var SENSITIVE_HEADER_KEYS = new Set([
|
|
524
|
+
"authorization",
|
|
525
|
+
"cookie",
|
|
526
|
+
"set-cookie",
|
|
527
|
+
"x-api-key",
|
|
528
|
+
"x-auth-token"
|
|
529
|
+
]);
|
|
530
|
+
var SENSITIVE_BODY_KEYS = new Set([
|
|
531
|
+
"password",
|
|
532
|
+
"token",
|
|
533
|
+
"accesstoken",
|
|
534
|
+
"access_token",
|
|
535
|
+
"refreshtoken",
|
|
536
|
+
"refresh_token",
|
|
537
|
+
"secret",
|
|
538
|
+
"client_secret",
|
|
539
|
+
"apikey",
|
|
540
|
+
"api_key"
|
|
541
|
+
]);
|
|
542
|
+
function redactHeaders(headers) {
|
|
543
|
+
if (!headers)
|
|
544
|
+
return;
|
|
545
|
+
const out = {};
|
|
546
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
547
|
+
if (SENSITIVE_HEADER_KEYS.has(k.toLowerCase())) {
|
|
548
|
+
out[k] = "[REDACTED]";
|
|
549
|
+
} else {
|
|
550
|
+
out[k] = v;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return out;
|
|
554
|
+
}
|
|
555
|
+
function redactBody(body) {
|
|
556
|
+
if (body === null || body === undefined)
|
|
557
|
+
return body;
|
|
558
|
+
if (typeof body !== "object" || Array.isArray(body)) {
|
|
559
|
+
return body;
|
|
560
|
+
}
|
|
561
|
+
const rec = body;
|
|
562
|
+
const out = { ...rec };
|
|
563
|
+
for (const key of Object.keys(out)) {
|
|
564
|
+
if (SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
|
|
565
|
+
out[key] = "[REDACTED]";
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return out;
|
|
569
|
+
}
|
|
570
|
+
function defaultRedactContext(ctx) {
|
|
571
|
+
return {
|
|
572
|
+
...ctx,
|
|
573
|
+
requestHeaders: redactHeaders(ctx.requestHeaders),
|
|
574
|
+
requestBody: redactBody(ctx.requestBody),
|
|
575
|
+
meta: ctx.meta
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// src/api-wrapper/error-reporting.ts
|
|
580
|
+
function applyErrorContextRedaction(ctx, config) {
|
|
581
|
+
const base = defaultRedactContext(ctx);
|
|
582
|
+
return config.redact ? config.redact(base) : base;
|
|
583
|
+
}
|
|
584
|
+
async function reportApiError(err, config) {
|
|
585
|
+
err.context = applyErrorContextRedaction(err.context, config);
|
|
586
|
+
try {
|
|
587
|
+
await config.onError?.(err);
|
|
588
|
+
} catch {}
|
|
589
|
+
config.logger?.error?.(err);
|
|
590
|
+
return err;
|
|
232
591
|
}
|
|
592
|
+
|
|
593
|
+
// src/api-wrapper/size.ts
|
|
233
594
|
function calculateSizeInMb(data) {
|
|
234
|
-
|
|
235
|
-
|
|
595
|
+
let bytes;
|
|
596
|
+
if (data === undefined || data === null) {
|
|
597
|
+
bytes = 0;
|
|
598
|
+
} else if (data instanceof Blob) {
|
|
599
|
+
bytes = data.size;
|
|
600
|
+
} else if (typeof FormData !== "undefined" && data instanceof FormData) {
|
|
601
|
+
bytes = 0;
|
|
602
|
+
data.forEach((value) => {
|
|
603
|
+
if (value instanceof Blob)
|
|
604
|
+
bytes += value.size;
|
|
605
|
+
else
|
|
606
|
+
bytes += new TextEncoder().encode(String(value)).length;
|
|
607
|
+
});
|
|
608
|
+
} else if (typeof data === "string") {
|
|
609
|
+
bytes = new TextEncoder().encode(data).length;
|
|
610
|
+
} else {
|
|
611
|
+
bytes = new TextEncoder().encode(JSON.stringify(data) ?? "").length;
|
|
612
|
+
}
|
|
613
|
+
return bytes / (1024 * 1024);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// src/api-wrapper/transport.ts
|
|
617
|
+
import { ZodError as ZodError3 } from "zod";
|
|
618
|
+
async function validateResponse(schema, data, ctx) {
|
|
619
|
+
try {
|
|
620
|
+
return await schema.parseAsync(data);
|
|
621
|
+
} catch (error) {
|
|
622
|
+
if (error instanceof ZodError3) {
|
|
623
|
+
throw new ResponseValidationError(error, data, ctx());
|
|
624
|
+
}
|
|
625
|
+
throw error;
|
|
626
|
+
}
|
|
236
627
|
}
|
|
237
|
-
function
|
|
628
|
+
async function httpRequest(config, ctx) {
|
|
629
|
+
const queryString = config.doNotEncodeQueryParams ? Object.entries(config.queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(config.queryParams).toString();
|
|
630
|
+
const fullUrl = queryString ? `${config.url}?${queryString}` : config.url;
|
|
631
|
+
const timeoutController = new AbortController;
|
|
632
|
+
const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => timeoutController.abort(), config.timeoutMs);
|
|
633
|
+
const signal = mergeSignals(timeoutController.signal, config.userSignal);
|
|
634
|
+
try {
|
|
635
|
+
return await config.fetchFn(fullUrl, {
|
|
636
|
+
method: config.method,
|
|
637
|
+
headers: config.headers,
|
|
638
|
+
body: buildFetchBody(config.body, config.headers),
|
|
639
|
+
signal
|
|
640
|
+
});
|
|
641
|
+
} catch (error) {
|
|
642
|
+
if (isDomAbortError(error)) {
|
|
643
|
+
if (config.userSignal?.aborted) {
|
|
644
|
+
throw new RequestAbortedError(ctx(), error);
|
|
645
|
+
}
|
|
646
|
+
if (timeoutController.signal.aborted && config.timeoutMs !== undefined) {
|
|
647
|
+
throw new RequestTimeoutError(config.timeoutMs, ctx());
|
|
648
|
+
}
|
|
649
|
+
throw new RequestAbortedError(ctx(), error);
|
|
650
|
+
}
|
|
651
|
+
throw new NetworkError(error instanceof Error ? error.message : String(error), ctx(), error);
|
|
652
|
+
} finally {
|
|
653
|
+
if (timeout)
|
|
654
|
+
clearTimeout(timeout);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function isDomAbortError(error) {
|
|
658
|
+
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
659
|
+
}
|
|
660
|
+
function mergeSignals(a, b) {
|
|
661
|
+
if (!b)
|
|
662
|
+
return a;
|
|
663
|
+
const anyFn = AbortSignal.any;
|
|
664
|
+
if (typeof anyFn === "function")
|
|
665
|
+
return anyFn([a, b]);
|
|
666
|
+
const controller = new AbortController;
|
|
667
|
+
const onAbort = () => controller.abort();
|
|
668
|
+
if (a.aborted)
|
|
669
|
+
controller.abort();
|
|
670
|
+
else
|
|
671
|
+
a.addEventListener("abort", onAbort, { once: true });
|
|
672
|
+
if (b.aborted)
|
|
673
|
+
controller.abort();
|
|
674
|
+
else
|
|
675
|
+
b.addEventListener("abort", onAbort, { once: true });
|
|
676
|
+
return controller.signal;
|
|
677
|
+
}
|
|
678
|
+
async function readErrorResponse(response) {
|
|
679
|
+
const body = await response.text();
|
|
680
|
+
return {
|
|
681
|
+
body,
|
|
682
|
+
headers: getHeadersAsObject(response.headers),
|
|
683
|
+
status: response.status
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
function getRetryDelay(params) {
|
|
687
|
+
const { retryDelay, attempt, response } = params;
|
|
688
|
+
if (response && (response.status === 429 || response.status === 503)) {
|
|
689
|
+
const retryAfter = response.headers.get("retry-after");
|
|
690
|
+
if (retryAfter) {
|
|
691
|
+
const seconds = Number(retryAfter);
|
|
692
|
+
if (Number.isFinite(seconds))
|
|
693
|
+
return seconds * 1000;
|
|
694
|
+
const dateMs = Date.parse(retryAfter);
|
|
695
|
+
if (!Number.isNaN(dateMs)) {
|
|
696
|
+
return Math.max(0, dateMs - Date.now());
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
if (typeof retryDelay === "function")
|
|
701
|
+
return retryDelay(attempt);
|
|
702
|
+
return retryDelay;
|
|
703
|
+
}
|
|
704
|
+
function shouldAttemptRetryOnError(error) {
|
|
705
|
+
if (error instanceof HttpResponseError)
|
|
706
|
+
return false;
|
|
707
|
+
if (error instanceof ResponseValidationError)
|
|
708
|
+
return false;
|
|
709
|
+
if (error instanceof RequestValidationError)
|
|
710
|
+
return false;
|
|
711
|
+
if (error instanceof ConfigError)
|
|
712
|
+
return false;
|
|
713
|
+
if (error instanceof RequestAbortedError)
|
|
714
|
+
return false;
|
|
715
|
+
return true;
|
|
716
|
+
}
|
|
717
|
+
function shouldAttemptRetryOnHttp(retryCtx) {
|
|
718
|
+
const { error, response } = retryCtx;
|
|
238
719
|
if (error)
|
|
239
|
-
return
|
|
240
|
-
return response ? response.status >= 500 : false;
|
|
720
|
+
return shouldAttemptRetryOnError(error);
|
|
721
|
+
return response ? response.status >= 500 || response.status === 429 : false;
|
|
722
|
+
}
|
|
723
|
+
async function makeRequestWithRetry(args) {
|
|
724
|
+
const {
|
|
725
|
+
config,
|
|
726
|
+
maxRetries,
|
|
727
|
+
retryDelay,
|
|
728
|
+
validateFn,
|
|
729
|
+
shouldRetry,
|
|
730
|
+
logger,
|
|
731
|
+
ctx,
|
|
732
|
+
endpointName,
|
|
733
|
+
method,
|
|
734
|
+
fullUrl,
|
|
735
|
+
meta,
|
|
736
|
+
onResponse,
|
|
737
|
+
onRetry
|
|
738
|
+
} = args;
|
|
739
|
+
let retryCount = 0;
|
|
740
|
+
while (true) {
|
|
741
|
+
let response;
|
|
742
|
+
try {
|
|
743
|
+
logger?.debug?.("API request", config);
|
|
744
|
+
response = await httpRequest(config, (over) => ctx({ attempt: retryCount, ...over }));
|
|
745
|
+
await onResponse?.({
|
|
746
|
+
endpoint: endpointName,
|
|
747
|
+
method,
|
|
748
|
+
url: fullUrl,
|
|
749
|
+
response: response.clone(),
|
|
750
|
+
attempt: retryCount,
|
|
751
|
+
meta
|
|
752
|
+
});
|
|
753
|
+
if (response.status >= 400) {
|
|
754
|
+
const retryCtx = {
|
|
755
|
+
response,
|
|
756
|
+
retryCount,
|
|
757
|
+
maxRetries,
|
|
758
|
+
endpoint: endpointName,
|
|
759
|
+
method,
|
|
760
|
+
url: fullUrl
|
|
761
|
+
};
|
|
762
|
+
if (retryCount < maxRetries && shouldRetry(retryCtx)) {
|
|
763
|
+
const delayMs = getRetryDelay({
|
|
764
|
+
retryDelay,
|
|
765
|
+
attempt: retryCount,
|
|
766
|
+
response
|
|
767
|
+
});
|
|
768
|
+
await onRetry?.({
|
|
769
|
+
...retryCtx,
|
|
770
|
+
delayMs,
|
|
771
|
+
nextAttempt: retryCount + 1
|
|
772
|
+
});
|
|
773
|
+
retryCount++;
|
|
774
|
+
await delay(delayMs);
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
const { body, headers: responseHeaders } = await readErrorResponse(response);
|
|
778
|
+
throw new HttpResponseError(response.status, body, responseHeaders, ctx({ attempt: retryCount }));
|
|
779
|
+
}
|
|
780
|
+
return {
|
|
781
|
+
validatedData: await validateFn(response),
|
|
782
|
+
retryCount,
|
|
783
|
+
httpStatus: response.status
|
|
784
|
+
};
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error instanceof HttpResponseError || error instanceof ResponseValidationError || error instanceof RequestValidationError || error instanceof ConfigError || error instanceof RequestAbortedError) {
|
|
787
|
+
throw error;
|
|
788
|
+
}
|
|
789
|
+
const retryCtx = {
|
|
790
|
+
error,
|
|
791
|
+
retryCount,
|
|
792
|
+
maxRetries,
|
|
793
|
+
endpoint: endpointName,
|
|
794
|
+
method,
|
|
795
|
+
url: fullUrl
|
|
796
|
+
};
|
|
797
|
+
if (retryCount >= maxRetries || !shouldRetry(retryCtx)) {
|
|
798
|
+
throw error;
|
|
799
|
+
}
|
|
800
|
+
const delayMs = getRetryDelay({
|
|
801
|
+
retryDelay,
|
|
802
|
+
attempt: retryCount,
|
|
803
|
+
response: undefined
|
|
804
|
+
});
|
|
805
|
+
await onRetry?.({
|
|
806
|
+
...retryCtx,
|
|
807
|
+
delayMs,
|
|
808
|
+
nextAttempt: retryCount + 1
|
|
809
|
+
});
|
|
810
|
+
retryCount++;
|
|
811
|
+
await delay(delayMs);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
async function validateAndTransformResponse(endpoint, response, ctx) {
|
|
816
|
+
const responseHeaders = getHeadersAsObject(response.headers);
|
|
817
|
+
let responseData;
|
|
818
|
+
if (hasJsonContentType(responseHeaders)) {
|
|
819
|
+
const text = await response.text();
|
|
820
|
+
try {
|
|
821
|
+
responseData = text === "" ? undefined : JSON.parse(text);
|
|
822
|
+
} catch (error) {
|
|
823
|
+
throw new ResponseValidationError(makeJsonParseZodError(error, text), text, ctx());
|
|
824
|
+
}
|
|
825
|
+
} else {
|
|
826
|
+
responseData = await response.text();
|
|
827
|
+
}
|
|
828
|
+
const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
|
|
829
|
+
return validateResponse(endpoint.resSchema, transformedData, ctx);
|
|
830
|
+
}
|
|
831
|
+
function defaultShouldRetry(ctx) {
|
|
832
|
+
if (ctx.error)
|
|
833
|
+
return shouldAttemptRetryOnError(ctx.error);
|
|
834
|
+
return shouldAttemptRetryOnHttp(ctx);
|
|
241
835
|
}
|
|
242
836
|
function delay(ms) {
|
|
243
837
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
244
838
|
}
|
|
839
|
+
|
|
840
|
+
// src/api-wrapper/create-client.ts
|
|
841
|
+
function isActionInvokeOptionsOnly(value) {
|
|
842
|
+
if (value === null || value === undefined)
|
|
843
|
+
return false;
|
|
844
|
+
if (typeof value !== "object" || Array.isArray(value))
|
|
845
|
+
return false;
|
|
846
|
+
return Object.keys(value).every((k) => k === "signal" || k === "meta");
|
|
847
|
+
}
|
|
848
|
+
function createApiClientImpl(apiConfig) {
|
|
849
|
+
let actions = {};
|
|
850
|
+
const cache = createActionCache();
|
|
851
|
+
const invokeEndpoint = async (endpointKey, input = {}) => {
|
|
852
|
+
const endpoint = apiConfig.endpoints[endpointKey];
|
|
853
|
+
const rawInput = input;
|
|
854
|
+
const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
|
|
855
|
+
const startedAt = Date.now();
|
|
856
|
+
const maxRetries = input.retries ?? apiConfig.retries ?? 3;
|
|
857
|
+
const baseContext = {
|
|
858
|
+
endpoint: endpointName,
|
|
859
|
+
method: endpoint?.method ?? "GET",
|
|
860
|
+
url: "",
|
|
861
|
+
attempt: 0,
|
|
862
|
+
maxRetries,
|
|
863
|
+
elapsedMs: 0,
|
|
864
|
+
meta: input.meta
|
|
865
|
+
};
|
|
866
|
+
const elapsed = () => Date.now() - startedAt;
|
|
867
|
+
const ctx = (over = {}) => ({
|
|
868
|
+
...baseContext,
|
|
869
|
+
elapsedMs: elapsed(),
|
|
870
|
+
...over
|
|
871
|
+
});
|
|
872
|
+
if (!endpoint) {
|
|
873
|
+
throw await reportApiError(new ConfigError(`Unknown API endpoint: ${String(endpointKey)}`, ctx()), apiConfig);
|
|
874
|
+
}
|
|
875
|
+
baseContext.method = endpoint.method;
|
|
876
|
+
let finalUrl;
|
|
877
|
+
let fullUrl;
|
|
878
|
+
try {
|
|
879
|
+
const built = buildUrl(endpoint, rawInput.reqParams, rawInput.reqQuery, apiConfig.baseApiUrls, apiConfig.defaultBaseApiUrl, input.url, (msg) => new ConfigError(msg, ctx()));
|
|
880
|
+
finalUrl = built.finalUrl;
|
|
881
|
+
fullUrl = built.fullUrl;
|
|
882
|
+
} catch (e) {
|
|
883
|
+
if (e instanceof ApiError)
|
|
884
|
+
throw await reportApiError(e, apiConfig);
|
|
885
|
+
throw e;
|
|
886
|
+
}
|
|
887
|
+
baseContext.url = fullUrl;
|
|
888
|
+
let queryParams;
|
|
889
|
+
let body;
|
|
890
|
+
let headers;
|
|
891
|
+
try {
|
|
892
|
+
queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx);
|
|
893
|
+
body = buildRequestBody(endpoint, rawInput.reqBody, ctx);
|
|
894
|
+
headers = buildHeaders(endpoint, rawInput.reqHeaders, apiConfig.defaultHeaders ?? {}, ctx);
|
|
895
|
+
} catch (e) {
|
|
896
|
+
if (e instanceof ApiError)
|
|
897
|
+
throw await reportApiError(e, apiConfig);
|
|
898
|
+
throw e;
|
|
899
|
+
}
|
|
900
|
+
baseContext.requestHeaders = headers;
|
|
901
|
+
baseContext.requestBody = body;
|
|
902
|
+
if (apiConfig.onRequest) {
|
|
903
|
+
try {
|
|
904
|
+
const result = await apiConfig.onRequest({
|
|
905
|
+
endpoint: endpointName,
|
|
906
|
+
method: endpoint.method,
|
|
907
|
+
url: fullUrl,
|
|
908
|
+
headers,
|
|
909
|
+
body,
|
|
910
|
+
meta: input.meta
|
|
911
|
+
});
|
|
912
|
+
if (result?.headers)
|
|
913
|
+
headers = result.headers;
|
|
914
|
+
if (result && "body" in result)
|
|
915
|
+
body = result.body;
|
|
916
|
+
} catch (e) {
|
|
917
|
+
if (e instanceof ApiError)
|
|
918
|
+
throw await reportApiError(e, apiConfig);
|
|
919
|
+
throw e;
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
const config = {
|
|
923
|
+
method: endpoint.method,
|
|
924
|
+
url: finalUrl,
|
|
925
|
+
queryParams,
|
|
926
|
+
body,
|
|
927
|
+
headers,
|
|
928
|
+
doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
|
|
929
|
+
timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
|
|
930
|
+
fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
|
|
931
|
+
userSignal: input.signal
|
|
932
|
+
};
|
|
933
|
+
try {
|
|
934
|
+
const result = await makeRequestWithRetry({
|
|
935
|
+
config,
|
|
936
|
+
maxRetries,
|
|
937
|
+
retryDelay: apiConfig.retryDelayMs ?? 300,
|
|
938
|
+
validateFn: (response) => validateAndTransformResponse(endpoint, response, ctx),
|
|
939
|
+
shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
|
|
940
|
+
logger: apiConfig.logger,
|
|
941
|
+
ctx,
|
|
942
|
+
endpointName,
|
|
943
|
+
method: endpoint.method,
|
|
944
|
+
fullUrl,
|
|
945
|
+
meta: input.meta,
|
|
946
|
+
onResponse: apiConfig.onResponse,
|
|
947
|
+
onRetry: apiConfig.onRetry
|
|
948
|
+
});
|
|
949
|
+
const responseTimeMs = elapsed();
|
|
950
|
+
const responseSizeMb = calculateSizeInMb(result.validatedData);
|
|
951
|
+
const requestBodySizeMb = calculateSizeInMb(config.body);
|
|
952
|
+
return {
|
|
953
|
+
data: result.validatedData,
|
|
954
|
+
requestBodySizeMb,
|
|
955
|
+
responseSizeMb,
|
|
956
|
+
responseTimeMs,
|
|
957
|
+
httpStatus: result.httpStatus,
|
|
958
|
+
retryCount: result.retryCount
|
|
959
|
+
};
|
|
960
|
+
} catch (error) {
|
|
961
|
+
if (error instanceof ApiError)
|
|
962
|
+
throw await reportApiError(error, apiConfig);
|
|
963
|
+
throw error;
|
|
964
|
+
}
|
|
965
|
+
};
|
|
966
|
+
async function invokeAction(name, rawInput, options = {}) {
|
|
967
|
+
const action = actions[name];
|
|
968
|
+
const inputForHandler = action.noRuntimeInput ? undefined : rawInput;
|
|
969
|
+
const actionName = apiConfig.name ? `${apiConfig.name}.${name}` : name;
|
|
970
|
+
const startedAt = Date.now();
|
|
971
|
+
const baseContext = {
|
|
972
|
+
endpoint: actionName,
|
|
973
|
+
method: "GET",
|
|
974
|
+
url: "",
|
|
975
|
+
attempt: 0,
|
|
976
|
+
maxRetries: 0,
|
|
977
|
+
elapsedMs: 0,
|
|
978
|
+
meta: options.meta,
|
|
979
|
+
requestBody: action.noRuntimeInput ? undefined : rawInput
|
|
980
|
+
};
|
|
981
|
+
const ctx = (over = {}) => ({
|
|
982
|
+
...baseContext,
|
|
983
|
+
elapsedMs: Date.now() - startedAt,
|
|
984
|
+
...over
|
|
985
|
+
});
|
|
986
|
+
apiConfig.logger?.debug?.("Action start", { action: actionName });
|
|
987
|
+
try {
|
|
988
|
+
const result = await action.handler({
|
|
989
|
+
input: inputForHandler,
|
|
990
|
+
api: proxy,
|
|
991
|
+
signal: options.signal,
|
|
992
|
+
meta: options.meta,
|
|
993
|
+
logger: apiConfig.logger,
|
|
994
|
+
cache
|
|
995
|
+
});
|
|
996
|
+
apiConfig.logger?.debug?.("Action end", {
|
|
997
|
+
action: actionName,
|
|
998
|
+
elapsedMs: Date.now() - startedAt
|
|
999
|
+
});
|
|
1000
|
+
return result;
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error instanceof ApiError) {
|
|
1003
|
+
throw await reportApiError(error, apiConfig);
|
|
1004
|
+
}
|
|
1005
|
+
const wrapped = new ActionError(`Action ${actionName} failed: ${error instanceof Error ? error.message : String(error)}`, ctx(), error);
|
|
1006
|
+
throw await reportApiError(wrapped, apiConfig);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
const request = async (key, input, options) => {
|
|
1010
|
+
if (key in actions) {
|
|
1011
|
+
const action = actions[key];
|
|
1012
|
+
if (action.noRuntimeInput) {
|
|
1013
|
+
if (input === undefined || input === null) {
|
|
1014
|
+
return invokeAction(key, undefined, options ?? {});
|
|
1015
|
+
}
|
|
1016
|
+
if (options !== undefined) {
|
|
1017
|
+
return invokeAction(key, undefined, options);
|
|
1018
|
+
}
|
|
1019
|
+
if (isActionInvokeOptionsOnly(input)) {
|
|
1020
|
+
return invokeAction(key, undefined, input);
|
|
1021
|
+
}
|
|
1022
|
+
return invokeAction(key, undefined, {});
|
|
1023
|
+
}
|
|
1024
|
+
return invokeAction(key, input, options);
|
|
1025
|
+
}
|
|
1026
|
+
return invokeEndpoint(key, input ?? {});
|
|
1027
|
+
};
|
|
1028
|
+
const client = { request, cache };
|
|
1029
|
+
const proxy = new Proxy(client, {
|
|
1030
|
+
get(target, prop, receiver) {
|
|
1031
|
+
if (prop in target || typeof prop !== "string") {
|
|
1032
|
+
return Reflect.get(target, prop, receiver);
|
|
1033
|
+
}
|
|
1034
|
+
if (prop in apiConfig.endpoints) {
|
|
1035
|
+
return (input) => invokeEndpoint(prop, input ?? {});
|
|
1036
|
+
}
|
|
1037
|
+
if (prop in actions) {
|
|
1038
|
+
const action = actions[prop];
|
|
1039
|
+
if (action.noRuntimeInput) {
|
|
1040
|
+
return (first, second) => {
|
|
1041
|
+
if (first === undefined || first === null) {
|
|
1042
|
+
return invokeAction(prop, undefined, second ?? {});
|
|
1043
|
+
}
|
|
1044
|
+
if (second !== undefined) {
|
|
1045
|
+
return invokeAction(prop, undefined, second);
|
|
1046
|
+
}
|
|
1047
|
+
if (isActionInvokeOptionsOnly(first)) {
|
|
1048
|
+
return invokeAction(prop, undefined, first);
|
|
1049
|
+
}
|
|
1050
|
+
return invokeAction(prop, undefined, {});
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
return (input, opt) => invokeAction(prop, input, opt);
|
|
1054
|
+
}
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
});
|
|
1058
|
+
const rawActions = apiConfig.actions;
|
|
1059
|
+
if (typeof rawActions === "function") {
|
|
1060
|
+
actions = rawActions({
|
|
1061
|
+
api: proxy,
|
|
1062
|
+
defineAction: makeDefineAction()
|
|
1063
|
+
});
|
|
1064
|
+
} else if (rawActions) {
|
|
1065
|
+
actions = rawActions;
|
|
1066
|
+
}
|
|
1067
|
+
const collisions = Object.keys(actions).filter((key) => (key in apiConfig.endpoints));
|
|
1068
|
+
if (collisions.length > 0) {
|
|
1069
|
+
throw new ConfigError(`Action name(s) collide with endpoint name(s): ${collisions.join(", ")}`, {
|
|
1070
|
+
endpoint: collisions[0],
|
|
1071
|
+
method: "GET",
|
|
1072
|
+
url: "",
|
|
1073
|
+
attempt: 0,
|
|
1074
|
+
maxRetries: 0,
|
|
1075
|
+
elapsedMs: 0
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
return proxy;
|
|
1079
|
+
}
|
|
1080
|
+
var createApiClient = (apiConfig) => createApiClientImpl(apiConfig);
|
|
245
1081
|
export {
|
|
246
|
-
|
|
1082
|
+
isResponseValidationError,
|
|
1083
|
+
isRequestValidationError,
|
|
1084
|
+
isRequestTimeoutError,
|
|
1085
|
+
isRequestAbortedError,
|
|
1086
|
+
isNetworkError,
|
|
1087
|
+
isHttpResponseError,
|
|
1088
|
+
isConfigError,
|
|
1089
|
+
isApiError,
|
|
1090
|
+
isActionError,
|
|
1091
|
+
defineAction,
|
|
247
1092
|
createApiClient,
|
|
248
1093
|
ResponseValidationError,
|
|
249
|
-
|
|
1094
|
+
RequestValidationError,
|
|
1095
|
+
RequestTimeoutError,
|
|
1096
|
+
RequestAbortedError,
|
|
1097
|
+
NetworkError,
|
|
1098
|
+
HttpResponseError,
|
|
1099
|
+
ConfigError,
|
|
1100
|
+
ApiError,
|
|
1101
|
+
ActionError
|
|
250
1102
|
};
|