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.
Files changed (40) hide show
  1. package/dist/api-wrapper/actions-impl.d.ts +4 -0
  2. package/dist/api-wrapper/builders.d.ts +12 -0
  3. package/dist/api-wrapper/create-client.d.ts +6 -0
  4. package/dist/api-wrapper/error-reporting.d.ts +7 -0
  5. package/dist/api-wrapper/errors.d.ts +63 -0
  6. package/dist/api-wrapper/headers.d.ts +9 -0
  7. package/dist/api-wrapper/index.d.ts +5 -84
  8. package/dist/api-wrapper/redaction.d.ts +2 -0
  9. package/dist/api-wrapper/shared.d.ts +16 -0
  10. package/dist/api-wrapper/size.d.ts +1 -0
  11. package/dist/api-wrapper/test-helpers.d.ts +6 -0
  12. package/dist/api-wrapper/transport.d.ts +52 -0
  13. package/dist/api-wrapper/types.d.ts +153 -0
  14. package/dist/api-wrapper/zod-issues.d.ts +17 -0
  15. package/dist/secrets/index.d.ts +10 -8
  16. package/dist/secrets/infisical.d.ts +19 -2
  17. package/dist/secrets/loaders.d.ts +16 -0
  18. package/dist/src/api-wrapper/index.js +1018 -166
  19. package/dist/src/index.js +1155 -232
  20. package/dist/src/secrets/index.js +1138 -230
  21. package/dist/src/secrets/infisical.js +1028 -173
  22. package/dist/src/secrets/loaders.js +1236 -0
  23. package/package.json +2 -2
  24. package/src/api-wrapper/actions-impl.ts +100 -0
  25. package/src/api-wrapper/builders.ts +175 -0
  26. package/src/api-wrapper/create-client.ts +373 -0
  27. package/src/api-wrapper/error-reporting.ts +31 -0
  28. package/src/api-wrapper/errors.ts +221 -0
  29. package/src/api-wrapper/headers.ts +66 -0
  30. package/src/api-wrapper/index.ts +5 -492
  31. package/src/api-wrapper/redaction.ts +61 -0
  32. package/src/api-wrapper/shared.ts +25 -0
  33. package/src/api-wrapper/size.ts +19 -0
  34. package/src/api-wrapper/test-helpers.ts +25 -0
  35. package/src/api-wrapper/transport.ts +354 -0
  36. package/src/api-wrapper/types.ts +258 -0
  37. package/src/api-wrapper/zod-issues.ts +157 -0
  38. package/src/secrets/index.ts +125 -81
  39. package/src/secrets/infisical.ts +50 -14
  40. package/src/secrets/loaders.ts +67 -0
@@ -1,156 +1,350 @@
1
- // src/api-wrapper/index.ts
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
+ }
191
+
192
+ // src/api-wrapper/errors.ts
193
+ var MAX_BODY_PREVIEW = 500;
3
194
 
4
- class ResponseValidationError extends Error {
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.issues.map((issue) => `${issue.path.join(".") || "<root>"}: ${issue.message}`).join("; ");
10
- super(`Response validation failed: ${issues}`);
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.validationIssues = issues;
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 HttpResponseError extends Error {
19
- status;
20
- body;
21
- constructor(status, body) {
22
- super(`HTTP error: ${status} - ${body}`);
23
- this.name = "HttpResponseError";
24
- this.status = status;
25
- this.body = body;
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
- function createApiClient(apiConfig) {
29
- const request = async (endpointKey, input = {}) => {
30
- const endpoint = apiConfig.endpoints[endpointKey];
31
- if (!endpoint) {
32
- throw new Error(`Unknown API endpoint: ${String(endpointKey)}`);
33
- }
34
- const { finalUrl, fullUrl } = buildUrl(endpoint, input.reqParams, input.reqQuery, apiConfig.baseApiUrls, apiConfig.defaultBaseApiUrl, input.url);
35
- const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
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
- var createApiWrapper = createApiClient;
86
- async function validateResponse(schema, data) {
87
- try {
88
- return await schema.parseAsync(data);
89
- } catch (error) {
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
- async function httpRequest(config) {
97
- const queryString = config.doNotEncodeQueryParams ? Object.entries(config.queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(config.queryParams).toString();
98
- const fullUrl = queryString ? `${config.url}?${queryString}` : config.url;
99
- const abortController = new AbortController;
100
- const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => abortController.abort(), config.timeoutMs);
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
- } finally {
109
- if (timeout)
110
- clearTimeout(timeout);
316
+ this.name = "RequestAbortedError";
111
317
  }
112
318
  }
113
- async function makeRequestWithRetry(config, maxRetries, retryDelayMs, validateFn, shouldRetry, logger) {
114
- let retryCount = 0;
115
- while (true) {
116
- try {
117
- logger?.debug?.("API request", config);
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
- async function validateAndTransformResponse(endpoint, response) {
144
- const responseHeaders = getHeadersAsObject(response.headers);
145
- const contentType = responseHeaders["content-type"] ?? "";
146
- const responseData = contentType.includes("application/json") ? await response.json() : await response.text();
147
- const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
148
- return validateResponse(endpoint.resSchema, transformedData);
326
+
327
+ class ActionError extends ApiError {
328
+ constructor(message, context, cause) {
329
+ super(message, context, { cause });
330
+ this.name = "ActionError";
331
+ }
149
332
  }
150
- function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url) {
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(buildQueryParams(endpoint, reqQuery)).toString();
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 new Error("At least one base API URL is required");
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 new Error(`Unknown base API URL: ${baseUrlKey}`);
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(buildQueryParams(endpoint, reqQuery)).toString();
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 buildQueryParams(endpoint, query) {
371
+ function buildQueryParamsRaw(endpoint, query) {
372
+ const defaultParams = endpoint.reqDefaultQueryParams ?? {};
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) {
178
377
  const defaultParams = endpoint.reqDefaultQueryParams ?? {};
179
- const parsedQuery = endpoint.reqQuerySchema ? endpoint.reqQuerySchema.parse(query) : query ?? {};
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
- const parsedBody = endpoint.reqBodySchema.parse(body);
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,673 @@ 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
- const parsedHeaders = endpoint.reqHeadersSchema ? endpoint.reqHeadersSchema.parse(headers) : headers ?? {};
201
- return {
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 new Error(`Missing required path parameter: ${key}`);
443
+ throw configError(`Missing required path parameter: ${key}`);
222
444
  }
223
445
  return encodeURIComponent(String(value));
224
446
  });
225
447
  }
226
- function getHeadersAsObject(headers) {
227
- const result = {};
228
- headers.forEach((value, key) => {
229
- result[key] = value;
230
- });
231
- return result;
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;
232
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;
591
+ }
592
+
593
+ // src/api-wrapper/size.ts
233
594
  function calculateSizeInMb(data) {
234
- const sizeInBytes = new TextEncoder().encode(JSON.stringify(data)).length;
235
- return sizeInBytes / (1024 * 1024);
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
+ }
627
+ }
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
+ };
236
685
  }
237
- function defaultShouldRetry({ error, response }) {
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 true;
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
  }
245
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);
246
1081
  // src/secrets/infisical.ts
247
1082
  import { z } from "zod";
1083
+ var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
248
1084
  var defaultFetch = globalThis.fetch;
249
1085
  var infisicalAuthResponseSchema = z.object({
250
1086
  accessToken: z.string()
@@ -256,19 +1092,14 @@ var infisicalSecretSchema = z.object({
256
1092
  var infisicalSecretsResponseSchema = z.object({
257
1093
  secrets: z.array(infisicalSecretSchema)
258
1094
  });
259
- async function loadInfisicalSecrets({
1095
+ async function fetchInfisicalSecrets({
260
1096
  keys,
261
1097
  projectId,
262
1098
  environment,
263
1099
  siteUrl,
264
- clientIdEnvKey,
265
- clientSecretEnvKey
1100
+ clientId,
1101
+ clientSecret
266
1102
  }) {
267
- const clientId = process.env[clientIdEnvKey];
268
- const clientSecret = process.env[clientSecretEnvKey];
269
- if (!clientId || !clientSecret) {
270
- throw new Error("Missing Infisical credentials");
271
- }
272
1103
  const infisical = createInfisicalApiClient(siteUrl);
273
1104
  const { data: auth } = await infisical.request("login", {
274
1105
  reqBody: {
@@ -292,6 +1123,28 @@ async function loadInfisicalSecrets({
292
1123
  const wantedKeys = new Set(keys);
293
1124
  return Object.fromEntries(data.secrets.filter((secret) => wantedKeys.has(secret.secretKey)).map((secret) => [secret.secretKey, secret.secretValue]));
294
1125
  }
1126
+ async function loadInfisicalSecrets({
1127
+ keys,
1128
+ projectId,
1129
+ environment,
1130
+ siteUrl,
1131
+ clientIdEnvKey,
1132
+ clientSecretEnvKey
1133
+ }) {
1134
+ const clientId = process.env[clientIdEnvKey];
1135
+ const clientSecret = process.env[clientSecretEnvKey];
1136
+ if (!clientId || !clientSecret) {
1137
+ throw new Error("Missing Infisical credentials");
1138
+ }
1139
+ return fetchInfisicalSecrets({
1140
+ keys,
1141
+ projectId,
1142
+ environment,
1143
+ siteUrl,
1144
+ clientId,
1145
+ clientSecret
1146
+ });
1147
+ }
295
1148
  function createInfisicalApiClient(siteUrl) {
296
1149
  return createApiClient({
297
1150
  name: "infisical",
@@ -335,6 +1188,48 @@ function getFetch() {
335
1188
  return globalThis.fetch ?? defaultFetch;
336
1189
  }
337
1190
 
1191
+ // src/secrets/loaders.ts
1192
+ function cloudflareWorkerEnvLoader(workerEnv) {
1193
+ return async (ctx) => {
1194
+ const out = {};
1195
+ for (const key of ctx.keys) {
1196
+ const str = workerBindingToString(workerEnv[key]);
1197
+ if (str !== undefined) {
1198
+ out[key] = str;
1199
+ }
1200
+ }
1201
+ return out;
1202
+ };
1203
+ }
1204
+ function workerBindingToString(value) {
1205
+ if (value === undefined || value === null) {
1206
+ return;
1207
+ }
1208
+ if (typeof value === "string") {
1209
+ return value === "" ? undefined : value;
1210
+ }
1211
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
1212
+ return String(value);
1213
+ }
1214
+ if (typeof value === "object") {
1215
+ return JSON.stringify(value);
1216
+ }
1217
+ return String(value);
1218
+ }
1219
+ function infisicalLoader(config) {
1220
+ return async (ctx) => fetchInfisicalSecrets({
1221
+ keys: ctx.keys,
1222
+ projectId: config.projectId,
1223
+ environment: ctx.environment,
1224
+ siteUrl: config.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
1225
+ clientId: config.clientId,
1226
+ clientSecret: config.clientSecret
1227
+ });
1228
+ }
1229
+ async function processEnvLoader(_ctx) {
1230
+ return {};
1231
+ }
1232
+
338
1233
  // src/secrets/vite.ts
339
1234
  function getViteDefine(secretSet) {
340
1235
  return Object.fromEntries(Object.entries(secretSet.secrets()).map(([key, value]) => [
@@ -351,9 +1246,6 @@ function viteSecretSetPlugin(secretSet) {
351
1246
  };
352
1247
  }
353
1248
 
354
- // src/secrets/index.ts
355
- import { networkInterfaces } from "node:os";
356
-
357
1249
  // src/cool-console-log/index.ts
358
1250
  var terminalColors = {
359
1251
  reset: "\x1B[0m",
@@ -460,9 +1352,6 @@ function logBanner(text, color = "cyan", options = {}) {
460
1352
  }
461
1353
 
462
1354
  // src/secrets/index.ts
463
- var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
464
- var LOCAL_IP_VALUE = "LOCAL_IP";
465
- var VPN_INTERFACE_RE = /^(utun|tun|tailscale|wg|ipsec|ppp)/i;
466
1355
  var logger = createCoolLogger();
467
1356
  function getSecretEnvironment() {
468
1357
  const env = process.env.SECRETS_ENV || "development";
@@ -474,61 +1363,91 @@ function getSecretEnvironment() {
474
1363
  }
475
1364
  async function defineSecretSet(keys, options) {
476
1365
  const keySet = [...new Set(keys)];
477
- const loader = options.loader ?? loadInfisicalSecrets;
478
- const getLocalIp = options.getLocalIp ?? getLocalIpAddress;
1366
+ const resolvedOptions = resolveSecretSetOptions(options);
479
1367
  let cachedSecrets = null;
480
- const resolveValue = (value) => {
481
- if (value === LOCAL_IP_VALUE)
482
- return getLocalIp();
483
- return value;
484
- };
485
1368
  const loadSecrets = async (force = false) => {
486
- const envSecrets = readEnvSecrets(keySet, force ? cachedSecrets : null);
487
- const keysMissingFromEnv = keySet.filter((key) => !envSecrets[key]);
488
- const environment = resolveEnvironment(options.environment);
489
- logSecretEnvironment(environment, keySet.length);
490
- const loadedSecrets = keysMissingFromEnv.length === 0 && !force ? {} : await loader({
491
- keys: keySet,
492
- projectId: options.projectId,
493
- environment,
494
- siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
495
- clientIdEnvKey: options.clientIdEnvKey ?? "INFISICAL_CLIENT_ID",
496
- clientSecretEnvKey: options.clientSecretEnvKey ?? "INFISICAL_CLIENT_SECRET"
1369
+ const nextSecrets = await loadAndMergeSecrets({
1370
+ keySet,
1371
+ cachedSecrets,
1372
+ force,
1373
+ ...resolvedOptions
497
1374
  });
498
- const missingKeys = keySet.filter((key) => !(envSecrets[key] ?? loadedSecrets[key]));
499
- if (missingKeys.length > 0) {
500
- throw new Error(`Missing secrets: ${missingKeys.join(", ")}`);
501
- }
502
- const nextSecrets = Object.fromEntries(keySet.map((key) => [
503
- key,
504
- resolveValue(envSecrets[key] ?? loadedSecrets[key])
505
- ]));
506
- for (const [key, value] of Object.entries(nextSecrets)) {
507
- process.env[key] = value;
508
- }
509
1375
  cachedSecrets = nextSecrets;
510
1376
  return nextSecrets;
511
1377
  };
1378
+ const secretSet = createSecretSet({
1379
+ getCachedSecrets: () => cachedSecrets,
1380
+ reload: loadSecrets
1381
+ });
1382
+ await loadSecrets();
1383
+ return secretSet;
1384
+ }
1385
+ function resolveSecretSetOptions(options) {
1386
+ const environment = resolveEnvironment(options.environment);
1387
+ const loader = options.loader ? options.loader : (context) => loadInfisicalSecrets({
1388
+ ...context,
1389
+ projectId: options.projectId,
1390
+ siteUrl: options.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
1391
+ clientIdEnvKey: options.clientIdEnvKey ?? "INFISICAL_CLIENT_ID",
1392
+ clientSecretEnvKey: options.clientSecretEnvKey ?? "INFISICAL_CLIENT_SECRET"
1393
+ });
1394
+ return {
1395
+ loader,
1396
+ environment
1397
+ };
1398
+ }
1399
+ async function loadAndMergeSecrets({
1400
+ keySet,
1401
+ cachedSecrets,
1402
+ loader,
1403
+ environment,
1404
+ force
1405
+ }) {
1406
+ const processEnvSecrets = readProcessEnvSecrets(keySet, force ? cachedSecrets : null);
1407
+ const keysMissingFromEnv = keySet.filter((key) => !processEnvSecrets[key]);
1408
+ logSecretEnvironment(environment, keySet.length);
1409
+ const loaderKeys = force ? keySet : keysMissingFromEnv;
1410
+ const loadedSecrets = loaderKeys.length === 0 ? {} : await loader({
1411
+ keys: loaderKeys,
1412
+ environment
1413
+ });
1414
+ const missingKeys = keySet.filter((key) => !(processEnvSecrets[key] ?? loadedSecrets[key]));
1415
+ if (missingKeys.length > 0) {
1416
+ throw new Error(`Missing secrets: ${missingKeys.join(", ")}`);
1417
+ }
1418
+ const nextSecrets = Object.fromEntries(keySet.map((key) => [
1419
+ key,
1420
+ processEnvSecrets[key] ?? loadedSecrets[key]
1421
+ ]));
1422
+ for (const [key, value] of Object.entries(nextSecrets)) {
1423
+ process.env[key] = value;
1424
+ }
1425
+ return nextSecrets;
1426
+ }
1427
+ function createSecretSet({
1428
+ getCachedSecrets,
1429
+ reload
1430
+ }) {
512
1431
  const secretSet = {
513
1432
  secret(key) {
514
- const value = cachedSecrets?.[key] ?? process.env[key];
1433
+ const value = getCachedSecrets()?.[key] ?? process.env[key];
515
1434
  if (!value) {
516
1435
  throw new Error(`Secret ${key} not found`);
517
1436
  }
518
- return resolveValue(value);
1437
+ return value;
519
1438
  },
520
1439
  secrets() {
1440
+ const cachedSecrets = getCachedSecrets();
521
1441
  if (!cachedSecrets) {
522
1442
  throw new Error("Secrets not initialized.");
523
1443
  }
524
- return cachedSecrets;
1444
+ return { ...cachedSecrets };
525
1445
  },
526
1446
  async reload() {
527
- await loadSecrets(true);
1447
+ await reload(true);
528
1448
  return secretSet;
529
1449
  }
530
1450
  };
531
- await loadSecrets();
532
1451
  return secretSet;
533
1452
  }
534
1453
  function resolveEnvironment(environment) {
@@ -542,32 +1461,21 @@ function logSecretEnvironment(environment, secretCount) {
542
1461
  count: secretCount
543
1462
  });
544
1463
  }
545
- function readEnvSecrets(keys, previousSecrets = null) {
1464
+ function readProcessEnvSecrets(keys, previousSecrets = null) {
546
1465
  return Object.fromEntries(keys.filter((key) => {
547
1466
  const value = process.env[key];
548
1467
  return value && value !== previousSecrets?.[key];
549
1468
  }).map((key) => [key, process.env[key]]));
550
1469
  }
551
- function getLocalIpAddress() {
552
- const interfaces = networkInterfaces();
553
- for (const name of Object.keys(interfaces)) {
554
- if (VPN_INTERFACE_RE.test(name))
555
- continue;
556
- const nets = interfaces[name];
557
- if (!nets)
558
- continue;
559
- for (const net of nets) {
560
- if (net.family === "IPv4" && !net.internal) {
561
- return net.address;
562
- }
563
- }
564
- }
565
- return "127.0.0.1";
566
- }
567
1470
  export {
568
1471
  viteSecretSetPlugin,
1472
+ processEnvLoader,
569
1473
  loadInfisicalSecrets,
1474
+ infisicalLoader,
570
1475
  getViteDefine,
571
1476
  getSecretEnvironment,
572
- defineSecretSet
1477
+ fetchInfisicalSecrets,
1478
+ defineSecretSet,
1479
+ cloudflareWorkerEnvLoader,
1480
+ DEFAULT_INFISICAL_SITE_URL
573
1481
  };