hanzio 1.0.10 → 1.2.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 (71) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/actions-impl.d.ts +4 -0
  3. package/dist/api-wrapper/builders.d.ts +23 -0
  4. package/dist/api-wrapper/create-client.d.ts +6 -0
  5. package/dist/api-wrapper/error-reporting.d.ts +7 -0
  6. package/dist/api-wrapper/errors.d.ts +64 -0
  7. package/dist/api-wrapper/headers.d.ts +10 -0
  8. package/dist/api-wrapper/index.d.ts +5 -84
  9. package/dist/api-wrapper/redaction.d.ts +2 -0
  10. package/dist/api-wrapper/shared.d.ts +16 -0
  11. package/dist/api-wrapper/size.d.ts +1 -0
  12. package/dist/api-wrapper/test-helpers.d.ts +6 -0
  13. package/dist/api-wrapper/transport.d.ts +55 -0
  14. package/dist/api-wrapper/types.d.ts +154 -0
  15. package/dist/api-wrapper/zod-issues.d.ts +17 -0
  16. package/dist/cache/index.d.ts +26 -0
  17. package/dist/secrets/core.d.ts +47 -0
  18. package/dist/secrets/deadline.d.ts +6 -0
  19. package/dist/secrets/environment.d.ts +9 -0
  20. package/dist/secrets/errors.d.ts +11 -0
  21. package/dist/secrets/index.d.ts +14 -23
  22. package/dist/secrets/infisical-cli.d.ts +6 -0
  23. package/dist/secrets/infisical-config.d.ts +17 -0
  24. package/dist/secrets/infisical-session.d.ts +4 -0
  25. package/dist/secrets/infisical.d.ts +14 -2
  26. package/dist/secrets/loaders.d.ts +9 -0
  27. package/dist/secrets/vite.d.ts +7 -3
  28. package/dist/secrets/worker-loader.d.ts +9 -0
  29. package/dist/secrets/worker.d.ts +3 -0
  30. package/dist/src/api-wrapper/index.js +1059 -177
  31. package/dist/src/index.js +2807 -1397
  32. package/dist/src/jwt/index.js +2 -2
  33. package/dist/src/p-queue/index.js +3 -3
  34. package/dist/src/secrets/index.js +1610 -407
  35. package/dist/src/secrets/infisical.js +1352 -214
  36. package/dist/src/secrets/loaders.js +1529 -0
  37. package/dist/src/secrets/vite.js +33 -11
  38. package/dist/src/secrets/worker.js +275 -0
  39. package/dist/src/sitemap/index.js +1 -1
  40. package/dist/src/zod/index.js +7 -7
  41. package/dist/typedswitch/index.d.ts +9 -5
  42. package/package.json +12 -3
  43. package/src/api-wrapper/actions-impl.ts +100 -0
  44. package/src/api-wrapper/builders.ts +200 -0
  45. package/src/api-wrapper/create-client.ts +393 -0
  46. package/src/api-wrapper/error-reporting.ts +31 -0
  47. package/src/api-wrapper/errors.ts +231 -0
  48. package/src/api-wrapper/headers.ts +79 -0
  49. package/src/api-wrapper/index.ts +5 -492
  50. package/src/api-wrapper/redaction.ts +62 -0
  51. package/src/api-wrapper/shared.ts +25 -0
  52. package/src/api-wrapper/size.ts +19 -0
  53. package/src/api-wrapper/test-helpers.ts +25 -0
  54. package/src/api-wrapper/transport.ts +350 -0
  55. package/src/api-wrapper/types.ts +259 -0
  56. package/src/api-wrapper/zod-issues.ts +157 -0
  57. package/src/cache/index.ts +228 -61
  58. package/src/secrets/core.ts +348 -0
  59. package/src/secrets/deadline.ts +69 -0
  60. package/src/secrets/environment.ts +69 -0
  61. package/src/secrets/errors.ts +29 -0
  62. package/src/secrets/index.ts +81 -140
  63. package/src/secrets/infisical-cli.ts +99 -0
  64. package/src/secrets/infisical-config.ts +70 -0
  65. package/src/secrets/infisical-session.ts +42 -0
  66. package/src/secrets/infisical.ts +219 -35
  67. package/src/secrets/loaders.ts +43 -0
  68. package/src/secrets/vite.ts +44 -15
  69. package/src/secrets/worker-loader.ts +56 -0
  70. package/src/secrets/worker.ts +4 -0
  71. package/src/typedswitch/index.ts +80 -69
@@ -1,297 +1,1415 @@
1
- // src/api-wrapper/index.ts
1
+ // src/secrets/infisical.ts
2
+ import { z as z2 } from "zod";
3
+ // src/api-wrapper/headers.ts
4
+ var CANONICAL_HEADER_NAMES = {
5
+ "content-type": "Content-Type",
6
+ authorization: "Authorization",
7
+ accept: "Accept",
8
+ "user-agent": "User-Agent",
9
+ "x-request-id": "X-Request-Id",
10
+ "x-correlation-id": "X-Correlation-Id"
11
+ };
12
+ function normalizeHeaders(headers) {
13
+ const result = {};
14
+ const lowerToCanonical = new Map;
15
+ for (const [key, value] of Object.entries(headers)) {
16
+ const lower = key.toLowerCase();
17
+ const canonical = CANONICAL_HEADER_NAMES[lower] ?? key;
18
+ const existing = lowerToCanonical.get(lower);
19
+ if (existing && existing !== canonical)
20
+ delete result[existing];
21
+ lowerToCanonical.set(lower, canonical);
22
+ result[canonical] = value;
23
+ }
24
+ return result;
25
+ }
26
+ function getHeader(headers, name) {
27
+ const lower = name.toLowerCase();
28
+ const canonical = CANONICAL_HEADER_NAMES[lower];
29
+ if (canonical !== undefined) {
30
+ if (headers[canonical] !== undefined)
31
+ return headers[canonical];
32
+ }
33
+ for (const [k, v] of Object.entries(headers)) {
34
+ if (k.toLowerCase() === lower)
35
+ return v;
36
+ }
37
+ return;
38
+ }
39
+ function hasJsonContentType(headers) {
40
+ const ct = getHeader(headers, "content-type") ?? "";
41
+ return ct.includes("application/json");
42
+ }
43
+ function buildFetchBody(body, headers, configError) {
44
+ if (body === undefined || body === null)
45
+ return;
46
+ if (body instanceof FormData || body instanceof Blob)
47
+ return body;
48
+ if (hasJsonContentType(headers))
49
+ return JSON.stringify(body);
50
+ if (typeof body === "object" && (Object.getPrototypeOf(body) === Object.prototype || Object.getPrototypeOf(body) === null) && configError) {
51
+ throw configError("Request body is a plain object but Content-Type is not application/json");
52
+ }
53
+ return body;
54
+ }
55
+ function getHeadersAsObject(headers) {
56
+ const result = {};
57
+ headers.forEach((value, key) => {
58
+ result[key] = value;
59
+ });
60
+ return result;
61
+ }
62
+
63
+ // src/api-wrapper/zod-issues.ts
2
64
  import { ZodError } from "zod";
65
+ function formatZodIssues(error, rawData) {
66
+ return error.issues.map((issue) => {
67
+ const pathArray = [...issue.path];
68
+ const path = formatIssuePath(pathArray);
69
+ const value = getValueAtPath(rawData, pathArray);
70
+ const valuePreview = value === undefined ? undefined : previewValue(value);
71
+ const raw = issue;
72
+ const expected = typeof raw.expected === "string" ? raw.expected : undefined;
73
+ const received = typeof raw.received === "string" ? raw.received : value !== undefined ? typeofVerbose(value) : undefined;
74
+ const known = new Set([
75
+ "code",
76
+ "path",
77
+ "message",
78
+ "expected",
79
+ "received",
80
+ "fatal"
81
+ ]);
82
+ const extra = {};
83
+ for (const [k, v] of Object.entries(raw)) {
84
+ if (!known.has(k) && v !== undefined)
85
+ extra[k] = v;
86
+ }
87
+ return {
88
+ path,
89
+ pathArray,
90
+ code: issue.code,
91
+ message: issue.message,
92
+ expected,
93
+ received,
94
+ value,
95
+ valuePreview,
96
+ extra: Object.keys(extra).length > 0 ? extra : undefined
97
+ };
98
+ });
99
+ }
100
+ function renderIssueSummary(issues) {
101
+ if (issues.length === 0)
102
+ return "(no issues reported)";
103
+ return issues.map((issue, idx) => {
104
+ const parts = [];
105
+ parts.push(` [${idx + 1}] ${issue.path}: ${issue.message}`);
106
+ if (issue.expected || issue.received) {
107
+ const exp = issue.expected ? `expected ${issue.expected}` : "";
108
+ const rec = issue.received ? `received ${issue.received}` : "";
109
+ parts.push(` (${[exp, rec].filter(Boolean).join(", ")})`);
110
+ }
111
+ if (issue.valuePreview !== undefined) {
112
+ parts.push(` value: ${issue.valuePreview}`);
113
+ }
114
+ if (issue.extra) {
115
+ for (const [k, v] of Object.entries(issue.extra)) {
116
+ parts.push(` ${k}: ${previewValue(v, 120)}`);
117
+ }
118
+ }
119
+ return parts.join(`
120
+ `);
121
+ }).join(`
122
+ `);
123
+ }
124
+ function formatIssuePath(pathArray) {
125
+ if (pathArray.length === 0)
126
+ return "<root>";
127
+ let out = "";
128
+ for (const segment of pathArray) {
129
+ if (typeof segment === "number") {
130
+ out += `[${segment}]`;
131
+ } else if (/^[A-Za-z_$][\w$]*$/.test(segment)) {
132
+ out += out === "" ? segment : `.${segment}`;
133
+ } else {
134
+ out += `[${JSON.stringify(segment)}]`;
135
+ }
136
+ }
137
+ return out;
138
+ }
139
+ function getValueAtPath(data, pathArray) {
140
+ let current = data;
141
+ for (const segment of pathArray) {
142
+ if (current === null || current === undefined)
143
+ return;
144
+ if (typeof current !== "object")
145
+ return;
146
+ current = current[segment];
147
+ }
148
+ return current;
149
+ }
150
+ function typeofVerbose(value) {
151
+ if (value === null)
152
+ return "null";
153
+ if (Array.isArray(value))
154
+ return "array";
155
+ return typeof value;
156
+ }
157
+ function previewValue(value, maxLen = 200) {
158
+ let str;
159
+ try {
160
+ if (value === undefined)
161
+ str = "undefined";
162
+ else if (typeof value === "string")
163
+ str = JSON.stringify(value);
164
+ else
165
+ str = JSON.stringify(value);
166
+ } catch {
167
+ str = String(value);
168
+ }
169
+ if (str === undefined)
170
+ str = String(value);
171
+ if (str.length > maxLen)
172
+ str = `${str.slice(0, maxLen)}…`;
173
+ return str;
174
+ }
175
+ function makeJsonParseZodError(error, text) {
176
+ const message = error instanceof Error ? `Response body was not valid JSON: ${error.message}` : "Response body was not valid JSON";
177
+ const preview = text.length > 200 ? `${text.slice(0, 200)}…` : text;
178
+ return new ZodError([
179
+ {
180
+ code: "custom",
181
+ path: [],
182
+ message: `${message} (body preview: ${JSON.stringify(preview)})`,
183
+ input: text
184
+ }
185
+ ]);
186
+ }
187
+ function tryParseJson(body) {
188
+ if (!body)
189
+ return;
190
+ try {
191
+ return JSON.parse(body);
192
+ } catch {
193
+ return;
194
+ }
195
+ }
196
+
197
+ // src/api-wrapper/errors.ts
198
+ var MAX_BODY_PREVIEW = 500;
3
199
 
4
- class ResponseValidationError extends Error {
200
+ class ApiError extends Error {
201
+ context;
202
+ cause;
203
+ constructor(message, context, options) {
204
+ super(message);
205
+ this.name = "ApiError";
206
+ this.context = context;
207
+ this.cause = options?.cause;
208
+ }
209
+ toJSON() {
210
+ return {
211
+ name: this.name,
212
+ message: this.message,
213
+ context: this.context
214
+ };
215
+ }
216
+ }
217
+
218
+ class HttpResponseError extends ApiError {
219
+ status;
220
+ body;
221
+ bodyJson;
222
+ responseHeaders;
223
+ requestId;
224
+ constructor(status, body, responseHeaders, context) {
225
+ const preview = body.length > MAX_BODY_PREVIEW ? `${body.slice(0, MAX_BODY_PREVIEW)}…` : body;
226
+ super(`HTTP ${status} ${context.method} ${context.url} - ${preview}`, context);
227
+ this.name = "HttpResponseError";
228
+ this.status = status;
229
+ this.body = body;
230
+ this.responseHeaders = responseHeaders;
231
+ this.requestId = getHeader(responseHeaders, "x-request-id") ?? getHeader(responseHeaders, "x-correlation-id");
232
+ this.bodyJson = tryParseJson(body);
233
+ }
234
+ toJSON() {
235
+ return {
236
+ ...super.toJSON(),
237
+ status: this.status,
238
+ body: this.body,
239
+ bodyJson: this.bodyJson,
240
+ requestId: this.requestId,
241
+ responseHeaders: this.responseHeaders
242
+ };
243
+ }
244
+ }
245
+
246
+ class ResponseValidationError extends ApiError {
5
247
  zodError;
6
248
  rawResponse;
249
+ issues;
7
250
  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}`);
251
+ constructor(zodError, rawResponse, context) {
252
+ const issues = formatZodIssues(zodError, rawResponse);
253
+ const summary = renderIssueSummary(issues);
254
+ super(`Response validation failed (${context.method} ${context.url}):
255
+ ${summary}`, context, { cause: zodError });
11
256
  this.name = "ResponseValidationError";
12
257
  this.zodError = zodError;
13
258
  this.rawResponse = rawResponse;
14
- this.validationIssues = issues;
259
+ this.issues = issues;
260
+ this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
261
+ }
262
+ toJSON() {
263
+ return {
264
+ ...super.toJSON(),
265
+ issues: this.issues,
266
+ validationIssues: this.validationIssues,
267
+ rawResponsePreview: previewValue(this.rawResponse, 1000)
268
+ };
15
269
  }
16
270
  }
17
271
 
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;
272
+ class RequestValidationError extends ApiError {
273
+ zodError;
274
+ target;
275
+ rawInput;
276
+ issues;
277
+ validationIssues;
278
+ constructor(zodError, target, rawInput, context) {
279
+ const issues = formatZodIssues(zodError, rawInput);
280
+ const summary = renderIssueSummary(issues);
281
+ super(`Request ${target} validation failed (${context.method} ${context.url}):
282
+ ${summary}`, context, { cause: zodError });
283
+ this.name = "RequestValidationError";
284
+ this.zodError = zodError;
285
+ this.target = target;
286
+ this.rawInput = rawInput;
287
+ this.issues = issues;
288
+ this.validationIssues = issues.map((i) => `${i.path}: ${i.message}`).join("; ");
289
+ }
290
+ toJSON() {
291
+ return {
292
+ ...super.toJSON(),
293
+ target: this.target,
294
+ issues: this.issues,
295
+ validationIssues: this.validationIssues
296
+ };
26
297
  }
27
298
  }
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)}`);
299
+
300
+ class RequestTimeoutError extends ApiError {
301
+ timeoutMs;
302
+ constructor(timeoutMs, context) {
303
+ super(`Request timed out after ${timeoutMs}ms: ${context.method} ${context.url}`, context);
304
+ this.name = "RequestTimeoutError";
305
+ this.timeoutMs = timeoutMs;
306
+ }
307
+ }
308
+
309
+ class NetworkError extends ApiError {
310
+ constructor(message, context, cause) {
311
+ super(message, context, { cause });
312
+ this.name = "NetworkError";
313
+ }
314
+ }
315
+
316
+ class RequestAbortedError extends ApiError {
317
+ constructor(context, cause) {
318
+ super(`Request aborted: ${context.method} ${context.url}`, context, {
319
+ cause
320
+ });
321
+ this.name = "RequestAbortedError";
322
+ }
323
+ }
324
+
325
+ class ConfigError extends ApiError {
326
+ constructor(message, context) {
327
+ super(message, context);
328
+ this.name = "ConfigError";
329
+ }
330
+ }
331
+
332
+ class ActionError extends ApiError {
333
+ constructor(message, context, cause) {
334
+ super(message, context, { cause });
335
+ this.name = "ActionError";
336
+ }
337
+ }
338
+ var isApiError = (e) => e instanceof ApiError;
339
+ var isHttpResponseError = (e) => e instanceof HttpResponseError;
340
+ var isResponseValidationError = (e) => e instanceof ResponseValidationError;
341
+ var isRequestValidationError = (e) => e instanceof RequestValidationError;
342
+ var isRequestTimeoutError = (e) => e instanceof RequestTimeoutError;
343
+ var isNetworkError = (e) => e instanceof NetworkError;
344
+ var isRequestAbortedError = (e) => e instanceof RequestAbortedError;
345
+ var isConfigError = (e) => e instanceof ConfigError;
346
+ var isActionError = (e) => e instanceof ActionError;
347
+ function isNonRetryableApiError(error) {
348
+ return error instanceof HttpResponseError || error instanceof ResponseValidationError || error instanceof RequestValidationError || error instanceof ConfigError || error instanceof RequestAbortedError;
349
+ }
350
+ // src/api-wrapper/builders.ts
351
+ import { ZodError as ZodError2 } from "zod";
352
+ function formatQueryString(queryParams, doNotEncode) {
353
+ if (Object.keys(queryParams).length === 0)
354
+ return "";
355
+ return doNotEncode ? Object.entries(queryParams).map(([key, value]) => `${key}=${value}`).join("&") : new URLSearchParams(queryParams).toString();
356
+ }
357
+ function buildUrl(options) {
358
+ const {
359
+ endpoint,
360
+ params,
361
+ queryParams,
362
+ baseApiUrls,
363
+ defaultBaseApiUrl,
364
+ url,
365
+ doNotEncodeQueryParams,
366
+ configError
367
+ } = options;
368
+ let finalUrl;
369
+ if (url) {
370
+ finalUrl = replacePathParams(url, params, configError);
371
+ } else {
372
+ const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
373
+ if (!defaultBaseUrl) {
374
+ throw configError("At least one base API URL is required");
33
375
  }
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 });
376
+ const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
377
+ const configBaseUrl = baseApiUrls[baseUrlKey];
378
+ if (!configBaseUrl) {
379
+ throw configError(`Unknown base API URL: ${baseUrlKey}`);
380
+ }
381
+ const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
382
+ const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
383
+ finalUrl = `${baseUrl}${replacePathParams(path, params, configError)}`;
384
+ }
385
+ const queryString = formatQueryString(queryParams, doNotEncodeQueryParams);
386
+ return {
387
+ finalUrl,
388
+ fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
389
+ };
390
+ }
391
+ function buildPathParams(endpoint, params, ctx) {
392
+ if (!endpoint.reqParamsSchema)
393
+ return params;
394
+ try {
395
+ return endpoint.reqParamsSchema.parse(params);
396
+ } catch (error) {
397
+ if (error instanceof ZodError2) {
398
+ throw new RequestValidationError(error, "params", params, ctx());
399
+ }
400
+ throw error;
401
+ }
402
+ }
403
+ function buildQueryParams(endpoint, query, ctx) {
404
+ const defaultParams = endpoint.reqDefaultQueryParams ?? {};
405
+ let parsedQuery;
406
+ if (endpoint.reqQuerySchema) {
40
407
  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
- };
408
+ parsedQuery = endpoint.reqQuerySchema.parse(query);
78
409
  } catch (error) {
79
- apiConfig.logger?.error?.(error);
410
+ if (error instanceof ZodError2) {
411
+ throw new RequestValidationError(error, "query", query, ctx());
412
+ }
80
413
  throw error;
81
414
  }
415
+ } else {
416
+ parsedQuery = query ?? {};
417
+ }
418
+ const merged = { ...defaultParams, ...parsedQuery };
419
+ return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
420
+ }
421
+ function buildRequestBody(endpoint, body, ctx) {
422
+ if (!endpoint.reqBodySchema)
423
+ return body;
424
+ let parsedBody;
425
+ try {
426
+ parsedBody = endpoint.reqBodySchema.parse(body);
427
+ } catch (error) {
428
+ if (error instanceof ZodError2) {
429
+ throw new RequestValidationError(error, "body", body, ctx());
430
+ }
431
+ throw error;
432
+ }
433
+ if (endpoint.reqBodyFormat !== "form-data") {
434
+ return parsedBody;
435
+ }
436
+ const formData = new FormData;
437
+ for (const [key, value] of Object.entries(parsedBody)) {
438
+ if (value == null)
439
+ continue;
440
+ formData.set(key, value instanceof Blob ? value : String(value));
441
+ }
442
+ return formData;
443
+ }
444
+ function buildHeaders(endpoint, headers, defaultHeaders, ctx) {
445
+ const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
446
+ let parsedHeaders;
447
+ if (endpoint.reqHeadersSchema) {
448
+ try {
449
+ parsedHeaders = endpoint.reqHeadersSchema.parse(headers);
450
+ } catch (error) {
451
+ if (error instanceof ZodError2) {
452
+ throw new RequestValidationError(error, "headers", headers, ctx());
453
+ }
454
+ throw error;
455
+ }
456
+ } else {
457
+ parsedHeaders = headers ?? {};
458
+ }
459
+ return normalizeHeaders({
460
+ ...defaultHeaders,
461
+ ...contentType && { "Content-Type": contentType },
462
+ ...endpoint.defaultHeaders,
463
+ ...parsedHeaders
464
+ });
465
+ }
466
+ function replacePathParams(path, params, configError) {
467
+ return path.replace(/:(\w+)/g, (_, key) => {
468
+ const value = params?.[key];
469
+ if (value === undefined) {
470
+ throw configError(`Missing required path parameter: ${key}`);
471
+ }
472
+ return encodeURIComponent(String(value));
473
+ });
474
+ }
475
+
476
+ // src/api-wrapper/actions-impl.ts
477
+ function createActionCache() {
478
+ const store = new Map;
479
+ const isExpired = (entry) => entry.expiresAt !== undefined && entry.expiresAt <= Date.now();
480
+ const cache = async (key, fn, options) => {
481
+ const existing = store.get(key);
482
+ if (existing && !isExpired(existing)) {
483
+ if (existing.inflight)
484
+ return existing.inflight;
485
+ if (existing.hasValue)
486
+ return existing.value;
487
+ }
488
+ const promise = (async () => fn())();
489
+ const entry = {
490
+ hasValue: false,
491
+ inflight: promise,
492
+ expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
493
+ };
494
+ store.set(key, entry);
495
+ try {
496
+ const value = await promise;
497
+ entry.value = value;
498
+ entry.hasValue = true;
499
+ entry.inflight = undefined;
500
+ return value;
501
+ } catch (error) {
502
+ store.delete(key);
503
+ throw error;
504
+ }
505
+ };
506
+ cache.get = (key) => {
507
+ const entry = store.get(key);
508
+ if (!entry || !entry.hasValue)
509
+ return;
510
+ if (isExpired(entry)) {
511
+ store.delete(key);
512
+ return;
513
+ }
514
+ return entry.value;
515
+ };
516
+ cache.set = (key, value, options) => {
517
+ store.set(key, {
518
+ value,
519
+ hasValue: true,
520
+ expiresAt: options?.ttlMs !== undefined ? Date.now() + options.ttlMs : undefined
521
+ });
82
522
  };
83
- return { request };
523
+ cache.invalidate = (key) => {
524
+ store.delete(key);
525
+ };
526
+ cache.clear = () => {
527
+ store.clear();
528
+ };
529
+ return cache;
530
+ }
531
+ function makeDefineAction() {
532
+ function defineAction(...args) {
533
+ if (args.length === 0) {
534
+ return (def) => ({
535
+ ...def,
536
+ noRuntimeInput: false
537
+ });
538
+ }
539
+ const def = args[0];
540
+ return {
541
+ ...def,
542
+ noRuntimeInput: true
543
+ };
544
+ }
545
+ return defineAction;
546
+ }
547
+ var defineAction = makeDefineAction();
548
+
549
+ // src/api-wrapper/redaction.ts
550
+ var SENSITIVE_HEADER_KEYS = new Set([
551
+ "authorization",
552
+ "cookie",
553
+ "set-cookie",
554
+ "x-api-key",
555
+ "x-auth-token"
556
+ ]);
557
+ var SENSITIVE_BODY_KEYS = new Set([
558
+ "password",
559
+ "token",
560
+ "accesstoken",
561
+ "access_token",
562
+ "refreshtoken",
563
+ "refresh_token",
564
+ "secret",
565
+ "client_secret",
566
+ "clientsecret",
567
+ "apikey",
568
+ "api_key"
569
+ ]);
570
+ function redactHeaders(headers) {
571
+ if (!headers)
572
+ return;
573
+ const out = {};
574
+ for (const [k, v] of Object.entries(headers)) {
575
+ if (SENSITIVE_HEADER_KEYS.has(k.toLowerCase())) {
576
+ out[k] = "[REDACTED]";
577
+ } else {
578
+ out[k] = v;
579
+ }
580
+ }
581
+ return out;
84
582
  }
85
- var createApiWrapper = createApiClient;
86
- async function validateResponse(schema, data) {
583
+ function redactBody(body) {
584
+ if (body === null || body === undefined)
585
+ return body;
586
+ if (typeof body !== "object" || Array.isArray(body)) {
587
+ return body;
588
+ }
589
+ const rec = body;
590
+ const out = { ...rec };
591
+ for (const key of Object.keys(out)) {
592
+ if (SENSITIVE_BODY_KEYS.has(key.toLowerCase())) {
593
+ out[key] = "[REDACTED]";
594
+ }
595
+ }
596
+ return out;
597
+ }
598
+ function defaultRedactContext(ctx) {
599
+ return {
600
+ ...ctx,
601
+ requestHeaders: redactHeaders(ctx.requestHeaders),
602
+ requestBody: redactBody(ctx.requestBody),
603
+ meta: ctx.meta
604
+ };
605
+ }
606
+
607
+ // src/api-wrapper/error-reporting.ts
608
+ function applyErrorContextRedaction(ctx, config) {
609
+ const base = defaultRedactContext(ctx);
610
+ return config.redact ? config.redact(base) : base;
611
+ }
612
+ async function reportApiError(err, config) {
613
+ err.context = applyErrorContextRedaction(err.context, config);
614
+ try {
615
+ await config.onError?.(err);
616
+ } catch {}
617
+ config.logger?.error?.(err);
618
+ return err;
619
+ }
620
+
621
+ // src/api-wrapper/size.ts
622
+ function calculateSizeInMb(data) {
623
+ let bytes;
624
+ if (data === undefined || data === null) {
625
+ bytes = 0;
626
+ } else if (data instanceof Blob) {
627
+ bytes = data.size;
628
+ } else if (typeof FormData !== "undefined" && data instanceof FormData) {
629
+ bytes = 0;
630
+ data.forEach((value) => {
631
+ if (value instanceof Blob)
632
+ bytes += value.size;
633
+ else
634
+ bytes += new TextEncoder().encode(String(value)).length;
635
+ });
636
+ } else if (typeof data === "string") {
637
+ bytes = new TextEncoder().encode(data).length;
638
+ } else {
639
+ bytes = new TextEncoder().encode(JSON.stringify(data) ?? "").length;
640
+ }
641
+ return bytes / (1024 * 1024);
642
+ }
643
+
644
+ // src/api-wrapper/transport.ts
645
+ import { ZodError as ZodError3 } from "zod";
646
+ async function validateResponse(schema, data, ctx) {
87
647
  try {
88
648
  return await schema.parseAsync(data);
89
649
  } catch (error) {
90
- if (error instanceof ZodError) {
91
- throw new ResponseValidationError(error, data);
650
+ if (error instanceof ZodError3) {
651
+ throw new ResponseValidationError(error, data, ctx());
92
652
  }
93
653
  throw error;
94
654
  }
95
655
  }
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);
656
+ async function httpRequest(config, ctx) {
657
+ const timeoutController = new AbortController;
658
+ const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => timeoutController.abort(), config.timeoutMs);
659
+ const signal = mergeSignals(timeoutController.signal, config.userSignal);
101
660
  try {
102
- return await config.fetchFn(fullUrl, {
661
+ return await config.fetchFn(config.fullUrl, {
103
662
  method: config.method,
104
663
  headers: config.headers,
105
- body: buildFetchBody(config.body, config.headers),
106
- signal: abortController.signal
664
+ body: buildFetchBody(config.body, config.headers, (msg) => new ConfigError(msg, ctx())),
665
+ signal
107
666
  });
667
+ } catch (error) {
668
+ if (isDomAbortError(error)) {
669
+ if (config.userSignal?.aborted) {
670
+ throw new RequestAbortedError(ctx(), error);
671
+ }
672
+ if (timeoutController.signal.aborted && config.timeoutMs !== undefined) {
673
+ throw new RequestTimeoutError(config.timeoutMs, ctx());
674
+ }
675
+ throw new RequestAbortedError(ctx(), error);
676
+ }
677
+ if (error instanceof ConfigError)
678
+ throw error;
679
+ throw new NetworkError(error instanceof Error ? error.message : String(error), ctx(), error);
108
680
  } finally {
109
681
  if (timeout)
110
682
  clearTimeout(timeout);
111
683
  }
112
684
  }
113
- async function makeRequestWithRetry(config, maxRetries, retryDelayMs, validateFn, shouldRetry, logger) {
685
+ function isDomAbortError(error) {
686
+ return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
687
+ }
688
+ function mergeSignals(a, b) {
689
+ if (!b)
690
+ return a;
691
+ const anyFn = AbortSignal.any;
692
+ if (typeof anyFn === "function")
693
+ return anyFn([a, b]);
694
+ const controller = new AbortController;
695
+ const onAbort = () => controller.abort();
696
+ if (a.aborted)
697
+ controller.abort();
698
+ else
699
+ a.addEventListener("abort", onAbort, { once: true });
700
+ if (b.aborted)
701
+ controller.abort();
702
+ else
703
+ b.addEventListener("abort", onAbort, { once: true });
704
+ return controller.signal;
705
+ }
706
+ async function readErrorResponse(response) {
707
+ const body = await response.text();
708
+ return {
709
+ body,
710
+ headers: getHeadersAsObject(response.headers),
711
+ status: response.status
712
+ };
713
+ }
714
+ function getRetryDelay(params) {
715
+ const maxDelay = params.maxRetryDelayMs ?? 30000;
716
+ const { retryDelay, attempt, response } = params;
717
+ if (response && (response.status === 429 || response.status === 503)) {
718
+ const retryAfter = response.headers.get("retry-after");
719
+ if (retryAfter) {
720
+ const seconds = Number(retryAfter);
721
+ if (Number.isFinite(seconds)) {
722
+ return Math.min(Math.max(0, seconds * 1000), maxDelay);
723
+ }
724
+ const dateMs = Date.parse(retryAfter);
725
+ if (!Number.isNaN(dateMs)) {
726
+ return Math.min(Math.max(0, dateMs - Date.now()), maxDelay);
727
+ }
728
+ }
729
+ }
730
+ const delayMs = typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
731
+ return Math.min(delayMs, maxDelay);
732
+ }
733
+ function shouldAttemptRetryOnError(error) {
734
+ return !isNonRetryableApiError(error);
735
+ }
736
+ function shouldAttemptRetryOnHttp(retryCtx) {
737
+ const { error, response } = retryCtx;
738
+ if (error)
739
+ return shouldAttemptRetryOnError(error);
740
+ return response ? response.status >= 500 || response.status === 429 : false;
741
+ }
742
+ async function makeRequestWithRetry(args) {
743
+ const {
744
+ config,
745
+ maxRetries,
746
+ retryDelay,
747
+ maxRetryDelayMs,
748
+ validateFn,
749
+ shouldRetry,
750
+ logger,
751
+ ctx,
752
+ endpointName,
753
+ method,
754
+ fullUrl,
755
+ meta,
756
+ onResponse,
757
+ onRetry
758
+ } = args;
114
759
  let retryCount = 0;
115
760
  while (true) {
761
+ let response;
116
762
  try {
117
763
  logger?.debug?.("API request", config);
118
- const response = await httpRequest(config);
764
+ response = await httpRequest(config, (over) => ctx({ attempt: retryCount, ...over }));
765
+ await onResponse?.({
766
+ endpoint: endpointName,
767
+ method,
768
+ url: fullUrl,
769
+ response: response.clone(),
770
+ attempt: retryCount,
771
+ meta
772
+ });
119
773
  if (response.status >= 400) {
120
- if (retryCount < maxRetries && shouldRetry({ response, retryCount, maxRetries })) {
774
+ const retryCtx = {
775
+ response,
776
+ retryCount,
777
+ maxRetries,
778
+ endpoint: endpointName,
779
+ method,
780
+ url: fullUrl
781
+ };
782
+ if (retryCount < maxRetries && shouldRetry(retryCtx)) {
783
+ const delayMs = getRetryDelay({
784
+ retryDelay,
785
+ attempt: retryCount,
786
+ response,
787
+ maxRetryDelayMs
788
+ });
789
+ await onRetry?.({
790
+ ...retryCtx,
791
+ delayMs,
792
+ nextAttempt: retryCount + 1
793
+ });
121
794
  retryCount++;
122
- await delay(retryDelayMs);
795
+ await delay(delayMs);
123
796
  continue;
124
797
  }
125
- throw new HttpResponseError(response.status, await response.text());
798
+ const { body, headers: responseHeaders } = await readErrorResponse(response);
799
+ throw new HttpResponseError(response.status, body, responseHeaders, ctx({ attempt: retryCount }));
126
800
  }
127
801
  return {
128
- validatedData: await validateFn(response),
802
+ validatedData: await validateFn(response, retryCount),
129
803
  retryCount,
130
804
  httpStatus: response.status
131
805
  };
132
806
  } catch (error) {
133
- if (error instanceof HttpResponseError)
807
+ if (isNonRetryableApiError(error)) {
134
808
  throw error;
135
- if (retryCount >= maxRetries || !shouldRetry({ error, retryCount, maxRetries })) {
809
+ }
810
+ const retryCtx = {
811
+ error,
812
+ retryCount,
813
+ maxRetries,
814
+ endpoint: endpointName,
815
+ method,
816
+ url: fullUrl
817
+ };
818
+ if (retryCount >= maxRetries || !shouldRetry(retryCtx)) {
136
819
  throw error;
137
820
  }
821
+ const delayMs = getRetryDelay({
822
+ retryDelay,
823
+ attempt: retryCount,
824
+ response: undefined,
825
+ maxRetryDelayMs
826
+ });
827
+ await onRetry?.({
828
+ ...retryCtx,
829
+ delayMs,
830
+ nextAttempt: retryCount + 1
831
+ });
138
832
  retryCount++;
139
- await delay(retryDelayMs);
833
+ await delay(delayMs);
140
834
  }
141
835
  }
142
836
  }
143
- async function validateAndTransformResponse(endpoint, response) {
837
+ async function validateAndTransformResponse(endpoint, response, ctx) {
144
838
  const responseHeaders = getHeadersAsObject(response.headers);
145
- const contentType = responseHeaders["content-type"] ?? "";
146
- const responseData = contentType.includes("application/json") ? await response.json() : await response.text();
839
+ const text = await response.text();
840
+ const responseSizeBytes = new TextEncoder().encode(text).length;
841
+ let responseData;
842
+ if (hasJsonContentType(responseHeaders)) {
843
+ try {
844
+ responseData = text === "" ? undefined : JSON.parse(text);
845
+ } catch (error) {
846
+ throw new ResponseValidationError(makeJsonParseZodError(error, text), text, ctx());
847
+ }
848
+ } else {
849
+ responseData = text;
850
+ }
147
851
  const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
148
- return validateResponse(endpoint.resSchema, transformedData);
852
+ const data = await validateResponse(endpoint.resSchema, transformedData, ctx);
853
+ return { data, responseSizeBytes };
149
854
  }
150
- function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url) {
151
- if (url) {
152
- const finalUrl2 = replacePathParams(url, params);
153
- const queryString2 = new URLSearchParams(buildQueryParams(endpoint, reqQuery)).toString();
154
- return {
155
- finalUrl: finalUrl2,
156
- fullUrl: queryString2 ? `${finalUrl2}?${queryString2}` : finalUrl2
157
- };
855
+ function defaultShouldRetry(ctx) {
856
+ if (ctx.error)
857
+ return shouldAttemptRetryOnError(ctx.error);
858
+ return shouldAttemptRetryOnHttp(ctx);
859
+ }
860
+ function delay(ms) {
861
+ return new Promise((resolve) => setTimeout(resolve, ms));
862
+ }
863
+
864
+ // src/api-wrapper/create-client.ts
865
+ function isActionInvokeOptionsOnly(value) {
866
+ if (value === null || value === undefined)
867
+ return false;
868
+ if (typeof value !== "object" || Array.isArray(value))
869
+ return false;
870
+ return Object.keys(value).every((k) => k === "signal" || k === "meta");
871
+ }
872
+ function resolveActionInvocation(action, actionName, first, second) {
873
+ if (!action.noRuntimeInput) {
874
+ return { input: first, options: second ?? {} };
158
875
  }
159
- const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
160
- if (!defaultBaseUrl) {
161
- throw new Error("At least one base API URL is required");
876
+ if (first === undefined || first === null) {
877
+ return { input: undefined, options: second ?? {} };
162
878
  }
163
- const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
164
- const configBaseUrl = baseApiUrls[baseUrlKey];
165
- if (!configBaseUrl) {
166
- throw new Error(`Unknown base API URL: ${baseUrlKey}`);
879
+ if (second !== undefined) {
880
+ return { input: undefined, options: second };
167
881
  }
168
- const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
169
- 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();
172
- return {
173
- finalUrl,
174
- fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
882
+ if (isActionInvokeOptionsOnly(first)) {
883
+ return { input: undefined, options: first };
884
+ }
885
+ throw new ConfigError(`Action "${actionName}" does not accept input; pass only optional { signal, meta }`, {
886
+ endpoint: actionName,
887
+ method: "GET",
888
+ url: "",
889
+ attempt: 0,
890
+ maxRetries: 0,
891
+ elapsedMs: 0
892
+ });
893
+ }
894
+ function createApiClientImpl(apiConfig) {
895
+ let actions = {};
896
+ const cache = createActionCache();
897
+ const invokeEndpoint = async (endpointKey, input = {}) => {
898
+ const endpoint = apiConfig.endpoints[endpointKey];
899
+ const rawInput = input;
900
+ const endpointName = apiConfig.name ? `${apiConfig.name}.${String(endpointKey)}` : String(endpointKey);
901
+ const startedAt = Date.now();
902
+ const maxRetries = input.retries ?? apiConfig.retries ?? 3;
903
+ const baseContext = {
904
+ endpoint: endpointName,
905
+ method: endpoint?.method ?? "GET",
906
+ url: "",
907
+ attempt: 0,
908
+ maxRetries,
909
+ elapsedMs: 0,
910
+ meta: input.meta
911
+ };
912
+ const elapsed = () => Date.now() - startedAt;
913
+ const ctx = (over = {}) => ({
914
+ ...baseContext,
915
+ elapsedMs: elapsed(),
916
+ ...over
917
+ });
918
+ if (!endpoint) {
919
+ throw await reportApiError(new ConfigError(`Unknown API endpoint: ${String(endpointKey)}`, ctx()), apiConfig);
920
+ }
921
+ baseContext.method = endpoint.method;
922
+ let pathParams;
923
+ let queryParams;
924
+ let body;
925
+ let headers;
926
+ try {
927
+ pathParams = buildPathParams(endpoint, rawInput.reqParams, ctx);
928
+ queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx);
929
+ body = buildRequestBody(endpoint, rawInput.reqBody, ctx);
930
+ headers = buildHeaders(endpoint, rawInput.reqHeaders, apiConfig.defaultHeaders ?? {}, ctx);
931
+ } catch (e) {
932
+ if (e instanceof ApiError)
933
+ throw await reportApiError(e, apiConfig);
934
+ throw e;
935
+ }
936
+ let fullUrl;
937
+ try {
938
+ const built = buildUrl({
939
+ endpoint,
940
+ params: pathParams,
941
+ queryParams,
942
+ baseApiUrls: apiConfig.baseApiUrls,
943
+ defaultBaseApiUrl: apiConfig.defaultBaseApiUrl,
944
+ url: input.url,
945
+ doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
946
+ configError: (msg) => new ConfigError(msg, ctx())
947
+ });
948
+ fullUrl = built.fullUrl;
949
+ } catch (e) {
950
+ if (e instanceof ApiError)
951
+ throw await reportApiError(e, apiConfig);
952
+ throw e;
953
+ }
954
+ baseContext.url = fullUrl;
955
+ baseContext.requestHeaders = headers;
956
+ baseContext.requestBody = body;
957
+ if (apiConfig.onRequest) {
958
+ try {
959
+ const result = await apiConfig.onRequest({
960
+ endpoint: endpointName,
961
+ method: endpoint.method,
962
+ url: fullUrl,
963
+ headers,
964
+ body,
965
+ meta: input.meta
966
+ });
967
+ if (result?.headers)
968
+ headers = result.headers;
969
+ if (result && "body" in result)
970
+ body = result.body;
971
+ } catch (e) {
972
+ if (e instanceof ApiError)
973
+ throw await reportApiError(e, apiConfig);
974
+ throw e;
975
+ }
976
+ }
977
+ const config = {
978
+ method: endpoint.method,
979
+ fullUrl,
980
+ body,
981
+ headers,
982
+ timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
983
+ fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
984
+ userSignal: input.signal
985
+ };
986
+ try {
987
+ const result = await makeRequestWithRetry({
988
+ config,
989
+ maxRetries,
990
+ retryDelay: apiConfig.retryDelayMs ?? 300,
991
+ maxRetryDelayMs: apiConfig.maxRetryDelayMs,
992
+ validateFn: (response, attempt) => validateAndTransformResponse(endpoint, response, (over) => ctx({ attempt, ...over })),
993
+ shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
994
+ logger: apiConfig.logger,
995
+ ctx,
996
+ endpointName,
997
+ method: endpoint.method,
998
+ fullUrl,
999
+ meta: input.meta,
1000
+ onResponse: apiConfig.onResponse,
1001
+ onRetry: apiConfig.onRetry
1002
+ });
1003
+ const responseTimeMs = elapsed();
1004
+ const responseSizeMb = result.validatedData.responseSizeBytes / (1024 * 1024);
1005
+ const requestBodySizeMb = calculateSizeInMb(config.body);
1006
+ return {
1007
+ data: result.validatedData.data,
1008
+ requestBodySizeMb,
1009
+ responseSizeMb,
1010
+ responseTimeMs,
1011
+ httpStatus: result.httpStatus,
1012
+ retryCount: result.retryCount
1013
+ };
1014
+ } catch (error) {
1015
+ if (error instanceof ApiError)
1016
+ throw await reportApiError(error, apiConfig);
1017
+ throw error;
1018
+ }
175
1019
  };
1020
+ async function invokeAction(name, rawInput, options = {}) {
1021
+ const action = actions[name];
1022
+ const inputForHandler = action.noRuntimeInput ? undefined : rawInput;
1023
+ const actionName = apiConfig.name ? `${apiConfig.name}.${name}` : name;
1024
+ const startedAt = Date.now();
1025
+ const baseContext = {
1026
+ endpoint: actionName,
1027
+ method: "GET",
1028
+ url: "",
1029
+ attempt: 0,
1030
+ maxRetries: 0,
1031
+ elapsedMs: 0,
1032
+ meta: options.meta,
1033
+ requestBody: action.noRuntimeInput ? undefined : rawInput
1034
+ };
1035
+ const ctx = (over = {}) => ({
1036
+ ...baseContext,
1037
+ elapsedMs: Date.now() - startedAt,
1038
+ ...over
1039
+ });
1040
+ apiConfig.logger?.debug?.("Action start", { action: actionName });
1041
+ try {
1042
+ const result = await action.handler({
1043
+ input: inputForHandler,
1044
+ api: proxy,
1045
+ signal: options.signal,
1046
+ meta: options.meta,
1047
+ logger: apiConfig.logger,
1048
+ cache
1049
+ });
1050
+ apiConfig.logger?.debug?.("Action end", {
1051
+ action: actionName,
1052
+ elapsedMs: Date.now() - startedAt
1053
+ });
1054
+ return result;
1055
+ } catch (error) {
1056
+ if (error instanceof ApiError) {
1057
+ throw await reportApiError(error, apiConfig);
1058
+ }
1059
+ const wrapped = new ActionError(`Action ${actionName} failed: ${error instanceof Error ? error.message : String(error)}`, ctx(), error);
1060
+ throw await reportApiError(wrapped, apiConfig);
1061
+ }
1062
+ }
1063
+ async function dispatchAction(name, first, second) {
1064
+ const action = actions[name];
1065
+ const { input, options } = resolveActionInvocation(action, name, first, second);
1066
+ return invokeAction(name, input, options);
1067
+ }
1068
+ const request = async (key, input, options) => {
1069
+ if (key in actions) {
1070
+ return dispatchAction(key, input, options);
1071
+ }
1072
+ return invokeEndpoint(key, input ?? {});
1073
+ };
1074
+ const client = { request, cache };
1075
+ const proxy = new Proxy(client, {
1076
+ get(target, prop, receiver) {
1077
+ if (prop in target || typeof prop !== "string") {
1078
+ return Reflect.get(target, prop, receiver);
1079
+ }
1080
+ if (prop in apiConfig.endpoints) {
1081
+ return (input) => invokeEndpoint(prop, input ?? {});
1082
+ }
1083
+ if (prop in actions) {
1084
+ return (first, second) => dispatchAction(prop, first, second);
1085
+ }
1086
+ return;
1087
+ }
1088
+ });
1089
+ const rawActions = apiConfig.actions;
1090
+ if (typeof rawActions === "function") {
1091
+ actions = rawActions({
1092
+ api: proxy,
1093
+ defineAction: makeDefineAction()
1094
+ });
1095
+ } else if (rawActions) {
1096
+ actions = rawActions;
1097
+ }
1098
+ const collisions = Object.keys(actions).filter((key) => (key in apiConfig.endpoints));
1099
+ if (collisions.length > 0) {
1100
+ throw new ConfigError(`Action name(s) collide with endpoint name(s): ${collisions.join(", ")}`, {
1101
+ endpoint: collisions[0],
1102
+ method: "GET",
1103
+ url: "",
1104
+ attempt: 0,
1105
+ maxRetries: 0,
1106
+ elapsedMs: 0
1107
+ });
1108
+ }
1109
+ return proxy;
176
1110
  }
177
- function buildQueryParams(endpoint, query) {
178
- const defaultParams = endpoint.reqDefaultQueryParams ?? {};
179
- const parsedQuery = endpoint.reqQuerySchema ? endpoint.reqQuerySchema.parse(query) : query ?? {};
180
- const merged = { ...defaultParams, ...parsedQuery };
181
- return Object.fromEntries(Object.entries(merged).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
1111
+ var createApiClient = (apiConfig) => createApiClientImpl(apiConfig);
1112
+ // src/secrets/errors.ts
1113
+ class SecretLoadError extends Error {
1114
+ code;
1115
+ operation;
1116
+ status;
1117
+ constructor(code, message, details = {}) {
1118
+ super(message);
1119
+ this.name = "SecretLoadError";
1120
+ this.code = code;
1121
+ this.operation = details.operation;
1122
+ this.status = details.status;
1123
+ }
182
1124
  }
183
- function buildRequestBody(endpoint, body) {
184
- if (!endpoint.reqBodySchema)
185
- return body;
186
- const parsedBody = endpoint.reqBodySchema.parse(body);
187
- if (endpoint.reqBodyFormat !== "form-data") {
188
- return parsedBody;
1125
+
1126
+ // src/secrets/deadline.ts
1127
+ async function withSecretDeadline(options, operation) {
1128
+ const timeoutMs = options.timeoutMs ?? 30000;
1129
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2147483647) {
1130
+ throw new SecretLoadError("CONFIGURATION", "Secret timeoutMs must be a positive finite timer duration.");
189
1131
  }
190
- const formData = new FormData;
191
- for (const [key, value] of Object.entries(parsedBody)) {
192
- if (value == null)
193
- continue;
194
- formData.set(key, value instanceof Blob ? value : String(value));
1132
+ const controller = new AbortController;
1133
+ const cancel = () => controller.abort(new SecretLoadError("ABORTED", "Secret loading was cancelled."));
1134
+ if (options.signal?.aborted) {
1135
+ cancel();
1136
+ } else {
1137
+ options.signal?.addEventListener("abort", cancel, { once: true });
1138
+ }
1139
+ const timer = setTimeout(() => controller.abort(new SecretLoadError("TIMEOUT", `Secret loading timed out after ${timeoutMs}ms.`)), timeoutMs);
1140
+ let rejectOnAbort = () => {};
1141
+ try {
1142
+ const aborted = new Promise((_, reject) => {
1143
+ rejectOnAbort = () => reject(controller.signal.reason);
1144
+ controller.signal.addEventListener("abort", rejectOnAbort, { once: true });
1145
+ });
1146
+ if (controller.signal.aborted) {
1147
+ throw controller.signal.reason;
1148
+ }
1149
+ return await Promise.race([operation(controller.signal), aborted]);
1150
+ } finally {
1151
+ clearTimeout(timer);
1152
+ options.signal?.removeEventListener("abort", cancel);
1153
+ controller.signal.removeEventListener("abort", rejectOnAbort);
195
1154
  }
196
- return formData;
197
- }
198
- function buildHeaders(endpoint, headers, defaultHeaders) {
199
- const contentType = endpoint.reqBodyFormat === "json" ? "application/json" : undefined;
200
- const parsedHeaders = endpoint.reqHeadersSchema ? endpoint.reqHeadersSchema.parse(headers) : headers ?? {};
201
- return {
202
- ...defaultHeaders,
203
- ...contentType && { "Content-Type": contentType },
204
- ...endpoint.defaultHeaders,
205
- ...parsedHeaders
206
- };
207
1155
  }
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;
1156
+
1157
+ // src/secrets/environment.ts
1158
+ function getProcessEnvironment() {
1159
+ return typeof process === "undefined" ? undefined : process.env;
216
1160
  }
217
- function replacePathParams(path, params) {
218
- return path.replace(/:(\w+)/g, (_, key) => {
219
- const value = params?.[key];
220
- if (value === undefined) {
221
- throw new Error(`Missing required path parameter: ${key}`);
1161
+ function resolveSecretEnvironment(option, env) {
1162
+ const explicit = typeof option === "function" ? option() : option;
1163
+ if (typeof explicit === "object" && explicit !== null) {
1164
+ if (typeof explicit.slug !== "string" || !/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(explicit.slug)) {
1165
+ throw new SecretLoadError("CONFIGURATION", "Custom secret environment requires a valid slug.");
222
1166
  }
223
- return encodeURIComponent(String(value));
224
- });
1167
+ return explicit.slug;
1168
+ }
1169
+ const selected = explicit ?? env?.SECRETS_ENV;
1170
+ if (selected !== undefined) {
1171
+ if (selected === "production" || selected === "prod") {
1172
+ return "prod";
1173
+ }
1174
+ if (selected === "development" || selected === "dev") {
1175
+ return "dev";
1176
+ }
1177
+ if (selected === "staging") {
1178
+ return "staging";
1179
+ }
1180
+ throw new SecretLoadError("CONFIGURATION", "Invalid secret environment. Use dev, staging, prod, or an explicit { slug } option.");
1181
+ }
1182
+ if (env?.NODE_ENV === "production" || env?.NODE_ENV === "prod") {
1183
+ return "prod";
1184
+ }
1185
+ if (env?.NODE_ENV === "staging") {
1186
+ return "staging";
1187
+ }
1188
+ return "dev";
225
1189
  }
226
- function getHeadersAsObject(headers) {
227
- const result = {};
228
- headers.forEach((value, key) => {
229
- result[key] = value;
1190
+
1191
+ // src/secrets/infisical-cli.ts
1192
+ async function readInfisicalCliToken(options) {
1193
+ const { execFile } = await import("node:child_process").catch(() => {
1194
+ throw new SecretLoadError("CLI", "Infisical CLI fallback requires a runtime with subprocess support. Provide Infisical client credentials or a custom secret loader.");
230
1195
  });
231
- return result;
1196
+ options.signal.throwIfAborted();
1197
+ const env = {
1198
+ ...getProcessEnvironment(),
1199
+ INFISICAL_DISABLE_UPDATE_CHECK: "true"
1200
+ };
1201
+ delete env.INFISICAL_TOKEN;
1202
+ delete env.INFISICAL_UNIVERSAL_AUTH_ACCESS_TOKEN;
1203
+ delete env.TOKEN;
1204
+ try {
1205
+ const stdout = await new Promise((resolve, reject) => {
1206
+ const child = execFile("infisical", [
1207
+ "user",
1208
+ "get",
1209
+ "token",
1210
+ "--plain",
1211
+ `--domain=${options.siteUrl}`,
1212
+ "--silent"
1213
+ ], {
1214
+ env,
1215
+ encoding: "utf8",
1216
+ shell: false,
1217
+ timeout: options.timeoutMs ?? 30000,
1218
+ signal: options.signal,
1219
+ killSignal: "SIGKILL",
1220
+ maxBuffer: 10485760,
1221
+ windowsHide: true
1222
+ }, (error, output) => {
1223
+ if (error) {
1224
+ reject(error);
1225
+ } else {
1226
+ resolve(output);
1227
+ }
1228
+ });
1229
+ child.stdin?.end();
1230
+ });
1231
+ return stdout.trim();
1232
+ } catch (error) {
1233
+ if (options.signal.aborted) {
1234
+ throw options.signal.reason;
1235
+ }
1236
+ const failure = error;
1237
+ if (failure?.code === "ENOENT") {
1238
+ throw new SecretLoadError("CLI", "Infisical CLI not found. Install infisical and ensure it is on PATH, or provide Infisical client credentials.");
1239
+ }
1240
+ if (failure?.code === "ERR_CHILD_PROCESS_STDIO_MAXBUFFER") {
1241
+ throw new SecretLoadError("CLI", "Infisical CLI output exceeded the 10 MiB limit.");
1242
+ }
1243
+ if (failure?.killed) {
1244
+ throw new SecretLoadError("TIMEOUT", `Infisical CLI timed out after ${(options.timeoutMs ?? 30000) / 1000} seconds or was terminated.`);
1245
+ }
1246
+ throw new SecretLoadError("CLI", `Infisical CLI session token failed. Run infisical login --domain=${options.siteUrl} and verify access to the configured organization, project, and environment.`);
1247
+ }
232
1248
  }
233
- function calculateSizeInMb(data) {
234
- const sizeInBytes = new TextEncoder().encode(JSON.stringify(data)).length;
235
- return sizeInBytes / (1024 * 1024);
1249
+
1250
+ // src/secrets/infisical-config.ts
1251
+ function validateInfisicalScope(options) {
1252
+ validateInfisicalOrganizationId(options.organizationId);
1253
+ let url;
1254
+ try {
1255
+ url = new URL(options.siteUrl);
1256
+ } catch {
1257
+ throw new SecretLoadError("CONFIGURATION", "Invalid Infisical siteUrl.");
1258
+ }
1259
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
1260
+ throw new SecretLoadError("CONFIGURATION", "Infisical siteUrl must be an HTTP(S) URL without credentials, query, or fragment.");
1261
+ }
1262
+ if (!options.projectId.trim() || !options.environment.trim() || !(options.secretPath ?? "/").startsWith("/")) {
1263
+ throw new SecretLoadError("CONFIGURATION", "Infisical requires a projectId, environment, and absolute secretPath.");
1264
+ }
236
1265
  }
237
- function defaultShouldRetry({ error, response }) {
238
- if (error)
239
- return true;
240
- return response ? response.status >= 500 : false;
1266
+ function validateInfisicalOrganizationId(organizationId) {
1267
+ if (typeof organizationId !== "string" || !organizationId.trim() || /\s/.test(organizationId)) {
1268
+ throw new SecretLoadError("CONFIGURATION", "Infisical organizationId is required and must be a nonempty ID without whitespace.");
1269
+ }
241
1270
  }
242
- function delay(ms) {
243
- return new Promise((resolve) => setTimeout(resolve, ms));
1271
+
1272
+ // src/secrets/infisical-session.ts
1273
+ import { z } from "zod";
1274
+ var sessionClaimsSchema = z.object({
1275
+ organizationId: z.string().optional(),
1276
+ subOrganizationId: z.string().optional(),
1277
+ exp: z.number().finite().optional()
1278
+ });
1279
+ function parseInfisicalSessionToken(token) {
1280
+ let claims;
1281
+ try {
1282
+ if (!/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(token)) {
1283
+ throw new Error("Invalid token format");
1284
+ }
1285
+ const payload = token.split(".")[1];
1286
+ const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/"));
1287
+ claims = sessionClaimsSchema.parse(JSON.parse(json));
1288
+ } catch {
1289
+ throw new SecretLoadError("AUTHENTICATION", "Infisical returned an invalid user session token. Run infisical login for the configured domain.");
1290
+ }
1291
+ if (claims.exp !== undefined && claims.exp <= Date.now() / 1000) {
1292
+ throw new SecretLoadError("AUTHENTICATION", "Infisical user session has expired. Run infisical login for the configured domain.");
1293
+ }
1294
+ return { organizationId: claims.subOrganizationId ?? claims.organizationId };
244
1295
  }
245
1296
 
246
1297
  // src/secrets/infisical.ts
247
- import { z } from "zod";
1298
+ var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
248
1299
  var defaultFetch = globalThis.fetch;
249
- var infisicalAuthResponseSchema = z.object({
250
- accessToken: z.string()
251
- });
252
- var infisicalSecretSchema = z.object({
253
- secretKey: z.string(),
254
- secretValue: z.string()
1300
+ var infisicalAuthResponseSchema = z2.object({ accessToken: z2.string() });
1301
+ var infisicalSecretSchema = z2.object({
1302
+ secretKey: z2.string(),
1303
+ secretValue: z2.string()
255
1304
  });
256
- var infisicalSecretsResponseSchema = z.object({
257
- secrets: z.array(infisicalSecretSchema)
1305
+ var infisicalSecretsResponseSchema = z2.object({
1306
+ secrets: z2.array(infisicalSecretSchema)
258
1307
  });
259
- async function loadInfisicalSecrets({
260
- keys,
261
- projectId,
262
- environment,
263
- siteUrl,
264
- clientIdEnvKey,
265
- clientSecretEnvKey
266
- }) {
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
- const infisical = createInfisicalApiClient(siteUrl);
273
- const { data: auth } = await infisical.request("login", {
274
- reqBody: {
275
- clientId,
276
- clientSecret
1308
+ async function fetchInfisicalSecrets(options) {
1309
+ validateInfisicalScope(options);
1310
+ return withSecretDeadline(options, async (signal) => {
1311
+ let operation = "login";
1312
+ try {
1313
+ const infisical = createInfisicalApiClient(options.siteUrl);
1314
+ const { data: auth } = await infisical.request("login", {
1315
+ signal,
1316
+ reqBody: {
1317
+ clientId: options.clientId,
1318
+ clientSecret: options.clientSecret
1319
+ }
1320
+ });
1321
+ signal.throwIfAborted();
1322
+ operation = "listSecrets";
1323
+ return await listInfisicalSecrets(infisical, auth.accessToken, {
1324
+ ...options,
1325
+ signal
1326
+ });
1327
+ } catch (error) {
1328
+ throwInfisicalHttpError(error, operation, signal);
277
1329
  }
278
1330
  });
1331
+ }
1332
+ async function loadInfisicalSecrets(options) {
1333
+ validateInfisicalScope(options);
1334
+ const env = getProcessEnvironment();
1335
+ const clientId = env?.[options.clientIdEnvKey];
1336
+ const clientSecret = env?.[options.clientSecretEnvKey];
1337
+ const mode = options.auth ?? "auto";
1338
+ if (!["auto", "http", "cli"].includes(mode)) {
1339
+ throw new SecretLoadError("CONFIGURATION", "Invalid Infisical auth mode.");
1340
+ }
1341
+ const useHttp = mode === "http" || mode === "auto" && Boolean(clientId && clientSecret);
1342
+ if (useHttp) {
1343
+ if (!clientId || !clientSecret) {
1344
+ throw new SecretLoadError("AUTHENTICATION", "Infisical HTTP authentication requires both client credentials.");
1345
+ }
1346
+ options.reportSource?.("Infisical HTTP");
1347
+ return fetchInfisicalSecrets({ ...options, clientId, clientSecret });
1348
+ }
1349
+ options.reportSource?.(`Infisical CLI session (organization=${options.organizationId} project=${options.projectId} environment=${options.environment})`);
1350
+ return withSecretDeadline(options, (signal) => fetchInfisicalSessionSecrets({ ...options, signal }));
1351
+ }
1352
+ async function fetchInfisicalSessionSecrets(options) {
1353
+ const { signal } = options;
1354
+ let token = await readInfisicalCliToken(options);
1355
+ signal.throwIfAborted();
1356
+ const session = parseInfisicalSessionToken(token);
1357
+ const infisical = createInfisicalApiClient(options.siteUrl);
1358
+ let operation = "selectOrganization";
1359
+ try {
1360
+ if (session.organizationId !== options.organizationId) {
1361
+ const { data } = await infisical.request("selectOrganization", {
1362
+ signal,
1363
+ reqHeaders: {
1364
+ Authorization: `Bearer ${token}`,
1365
+ "User-Agent": "hanzio/secrets"
1366
+ },
1367
+ reqBody: { organizationId: options.organizationId }
1368
+ });
1369
+ signal.throwIfAborted();
1370
+ if (data.isMfaEnabled) {
1371
+ throw new SecretLoadError("AUTHENTICATION", "Infisical requires MFA for this organization. Complete infisical login for the target organization and configured domain before loading secrets.", { operation });
1372
+ }
1373
+ const scopedSession = parseInfisicalSessionToken(data.token);
1374
+ if (scopedSession.organizationId !== options.organizationId) {
1375
+ throw new SecretLoadError("AUTHENTICATION", "Infisical returned a session for a different organization.", { operation });
1376
+ }
1377
+ token = data.token;
1378
+ }
1379
+ signal.throwIfAborted();
1380
+ operation = "listSecrets";
1381
+ return await listInfisicalSecrets(infisical, token, options);
1382
+ } catch (error) {
1383
+ throwInfisicalHttpError(error, operation, signal);
1384
+ }
1385
+ }
1386
+ async function listInfisicalSecrets(infisical, token, options) {
279
1387
  const { data } = await infisical.request("listSecrets", {
280
- reqHeaders: {
281
- Authorization: `Bearer ${auth.accessToken}`
282
- },
1388
+ signal: options.signal,
1389
+ reqHeaders: { Authorization: `Bearer ${token}` },
283
1390
  reqQuery: {
284
- environment,
285
- projectId,
286
- secretPath: "/",
1391
+ environment: options.environment,
1392
+ projectId: options.projectId,
1393
+ secretPath: options.secretPath ?? "/",
287
1394
  viewSecretValue: true,
288
1395
  expandSecretReferences: true,
289
1396
  includeImports: true
290
1397
  }
291
1398
  });
292
- const wantedKeys = new Set(keys);
1399
+ const wantedKeys = new Set(options.keys);
293
1400
  return Object.fromEntries(data.secrets.filter((secret) => wantedKeys.has(secret.secretKey)).map((secret) => [secret.secretKey, secret.secretValue]));
294
1401
  }
1402
+ function throwInfisicalHttpError(error, operation, signal) {
1403
+ if (signal.aborted) {
1404
+ throw signal.reason;
1405
+ }
1406
+ if (error instanceof SecretLoadError) {
1407
+ throw error;
1408
+ }
1409
+ const status = error instanceof HttpResponseError ? error.status : undefined;
1410
+ const guidance = operation === "selectOrganization" && (status === 401 || status === 403) ? " Run infisical login for the target organization and configured domain, then verify your access." : "";
1411
+ throw new SecretLoadError("HTTP", `Infisical HTTP ${operation} failed${status ? ` (status ${status})` : ""}.${guidance}`, { operation, status });
1412
+ }
295
1413
  function createInfisicalApiClient(siteUrl) {
296
1414
  return createApiClient({
297
1415
  name: "infisical",
@@ -302,12 +1420,26 @@ function createInfisicalApiClient(siteUrl) {
302
1420
  fetch: getFetch(),
303
1421
  retries: 1,
304
1422
  endpoints: {
1423
+ selectOrganization: {
1424
+ method: "POST",
1425
+ path: "/api/v3/auth/select-organization",
1426
+ reqHeadersSchema: z2.object({
1427
+ Authorization: z2.string(),
1428
+ "User-Agent": z2.string()
1429
+ }),
1430
+ reqBodySchema: z2.object({ organizationId: z2.string() }),
1431
+ resSchema: z2.object({
1432
+ token: z2.string().min(1),
1433
+ isMfaEnabled: z2.boolean().optional()
1434
+ }),
1435
+ reqBodyFormat: "json"
1436
+ },
305
1437
  login: {
306
1438
  method: "POST",
307
1439
  path: "/api/v1/auth/universal-auth/login",
308
- reqBodySchema: z.object({
309
- clientId: z.string(),
310
- clientSecret: z.string()
1440
+ reqBodySchema: z2.object({
1441
+ clientId: z2.string(),
1442
+ clientSecret: z2.string()
311
1443
  }),
312
1444
  resSchema: infisicalAuthResponseSchema,
313
1445
  reqBodyFormat: "json"
@@ -315,16 +1447,16 @@ function createInfisicalApiClient(siteUrl) {
315
1447
  listSecrets: {
316
1448
  method: "GET",
317
1449
  path: "/api/v4/secrets",
318
- reqHeadersSchema: z.object({
319
- Authorization: z.string()
1450
+ reqHeadersSchema: z2.object({
1451
+ Authorization: z2.string()
320
1452
  }),
321
- reqQuerySchema: z.object({
322
- projectId: z.string(),
323
- environment: z.string(),
324
- secretPath: z.string(),
325
- viewSecretValue: z.boolean(),
326
- expandSecretReferences: z.boolean(),
327
- includeImports: z.boolean()
1453
+ reqQuerySchema: z2.object({
1454
+ projectId: z2.string(),
1455
+ environment: z2.string(),
1456
+ secretPath: z2.string(),
1457
+ viewSecretValue: z2.boolean(),
1458
+ expandSecretReferences: z2.boolean(),
1459
+ includeImports: z2.boolean()
328
1460
  }),
329
1461
  resSchema: infisicalSecretsResponseSchema
330
1462
  }
@@ -332,8 +1464,14 @@ function createInfisicalApiClient(siteUrl) {
332
1464
  });
333
1465
  }
334
1466
  function getFetch() {
335
- return globalThis.fetch ?? defaultFetch;
1467
+ const fetchFn = globalThis.fetch ?? defaultFetch;
1468
+ return (input, init) => {
1469
+ init?.signal?.throwIfAborted();
1470
+ return fetchFn(input, init);
1471
+ };
336
1472
  }
337
1473
  export {
1474
+ DEFAULT_INFISICAL_SITE_URL,
1475
+ fetchInfisicalSecrets,
338
1476
  loadInfisicalSecrets
339
1477
  };