hanzio 1.1.0 → 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 (55) hide show
  1. package/README.md +70 -4
  2. package/dist/api-wrapper/builders.d.ts +13 -2
  3. package/dist/api-wrapper/errors.d.ts +1 -0
  4. package/dist/api-wrapper/headers.d.ts +2 -1
  5. package/dist/api-wrapper/transport.d.ts +8 -5
  6. package/dist/api-wrapper/types.d.ts +1 -0
  7. package/dist/cache/index.d.ts +26 -0
  8. package/dist/secrets/core.d.ts +47 -0
  9. package/dist/secrets/deadline.d.ts +6 -0
  10. package/dist/secrets/environment.d.ts +9 -0
  11. package/dist/secrets/errors.d.ts +11 -0
  12. package/dist/secrets/index.d.ts +13 -25
  13. package/dist/secrets/infisical-cli.d.ts +6 -0
  14. package/dist/secrets/infisical-config.d.ts +17 -0
  15. package/dist/secrets/infisical-session.d.ts +4 -0
  16. package/dist/secrets/infisical.d.ts +8 -13
  17. package/dist/secrets/loaders.d.ts +5 -12
  18. package/dist/secrets/vite.d.ts +7 -3
  19. package/dist/secrets/worker-loader.d.ts +9 -0
  20. package/dist/secrets/worker.d.ts +3 -0
  21. package/dist/src/api-wrapper/index.js +152 -122
  22. package/dist/src/index.js +3173 -2713
  23. package/dist/src/jwt/index.js +2 -2
  24. package/dist/src/p-queue/index.js +3 -3
  25. package/dist/src/secrets/index.js +670 -402
  26. package/dist/src/secrets/infisical.js +451 -168
  27. package/dist/src/secrets/loaders.js +467 -174
  28. package/dist/src/secrets/vite.js +33 -11
  29. package/dist/src/secrets/worker.js +275 -0
  30. package/dist/src/sitemap/index.js +1 -1
  31. package/dist/src/zod/index.js +7 -7
  32. package/dist/typedswitch/index.d.ts +9 -5
  33. package/package.json +12 -3
  34. package/src/api-wrapper/builders.ts +68 -43
  35. package/src/api-wrapper/create-client.ts +82 -62
  36. package/src/api-wrapper/errors.ts +10 -0
  37. package/src/api-wrapper/headers.ts +14 -1
  38. package/src/api-wrapper/redaction.ts +1 -0
  39. package/src/api-wrapper/transport.ts +34 -38
  40. package/src/api-wrapper/types.ts +1 -0
  41. package/src/cache/index.ts +228 -61
  42. package/src/secrets/core.ts +348 -0
  43. package/src/secrets/deadline.ts +69 -0
  44. package/src/secrets/environment.ts +69 -0
  45. package/src/secrets/errors.ts +29 -0
  46. package/src/secrets/index.ts +79 -210
  47. package/src/secrets/infisical-cli.ts +99 -0
  48. package/src/secrets/infisical-config.ts +70 -0
  49. package/src/secrets/infisical-session.ts +42 -0
  50. package/src/secrets/infisical.ts +214 -66
  51. package/src/secrets/loaders.ts +24 -48
  52. package/src/secrets/vite.ts +44 -15
  53. package/src/secrets/worker-loader.ts +56 -0
  54. package/src/secrets/worker.ts +4 -0
  55. package/src/typedswitch/index.ts +80 -69
@@ -1,3 +1,5 @@
1
+ // src/secrets/infisical.ts
2
+ import { z as z2 } from "zod";
1
3
  // src/api-wrapper/headers.ts
2
4
  var CANONICAL_HEADER_NAMES = {
3
5
  "content-type": "Content-Type",
@@ -38,13 +40,16 @@ function hasJsonContentType(headers) {
38
40
  const ct = getHeader(headers, "content-type") ?? "";
39
41
  return ct.includes("application/json");
40
42
  }
41
- function buildFetchBody(body, headers) {
43
+ function buildFetchBody(body, headers, configError) {
42
44
  if (body === undefined || body === null)
43
45
  return;
44
46
  if (body instanceof FormData || body instanceof Blob)
45
47
  return body;
46
48
  if (hasJsonContentType(headers))
47
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
+ }
48
53
  return body;
49
54
  }
50
55
  function getHeadersAsObject(headers) {
@@ -339,39 +344,61 @@ var isNetworkError = (e) => e instanceof NetworkError;
339
344
  var isRequestAbortedError = (e) => e instanceof RequestAbortedError;
340
345
  var isConfigError = (e) => e instanceof ConfigError;
341
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
+ }
342
350
  // src/api-wrapper/builders.ts
343
351
  import { ZodError as ZodError2 } from "zod";
344
- function buildUrl(endpoint, params, reqQuery, baseApiUrls, defaultBaseApiUrl, url, configError) {
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;
345
369
  if (url) {
346
- const finalUrl2 = replacePathParams(url, params, configError);
347
- const queryString2 = new URLSearchParams(buildQueryParamsRaw(endpoint, reqQuery)).toString();
348
- return {
349
- finalUrl: finalUrl2,
350
- fullUrl: queryString2 ? `${finalUrl2}?${queryString2}` : finalUrl2
351
- };
352
- }
353
- const defaultBaseUrl = defaultBaseApiUrl ?? Object.keys(baseApiUrls)[0];
354
- if (!defaultBaseUrl) {
355
- throw configError("At least one base API URL is required");
356
- }
357
- const baseUrlKey = endpoint.baseApiUrl ?? defaultBaseUrl;
358
- const configBaseUrl = baseApiUrls[baseUrlKey];
359
- if (!configBaseUrl) {
360
- throw configError(`Unknown base API URL: ${baseUrlKey}`);
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");
375
+ }
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)}`;
361
384
  }
362
- const baseUrl = typeof configBaseUrl === "function" ? configBaseUrl() : configBaseUrl;
363
- const path = typeof endpoint.path === "function" ? endpoint.path(baseUrl) : endpoint.path;
364
- const finalUrl = replacePathParams(`${baseUrl}${path}`, params, configError);
365
- const queryString = new URLSearchParams(buildQueryParamsRaw(endpoint, reqQuery)).toString();
385
+ const queryString = formatQueryString(queryParams, doNotEncodeQueryParams);
366
386
  return {
367
387
  finalUrl,
368
388
  fullUrl: queryString ? `${finalUrl}?${queryString}` : finalUrl
369
389
  };
370
390
  }
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)]));
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
+ }
375
402
  }
376
403
  function buildQueryParams(endpoint, query, ctx) {
377
404
  const defaultParams = endpoint.reqDefaultQueryParams ?? {};
@@ -504,8 +531,8 @@ function createActionCache() {
504
531
  function makeDefineAction() {
505
532
  function defineAction(...args) {
506
533
  if (args.length === 0) {
507
- return (def2) => ({
508
- ...def2,
534
+ return (def) => ({
535
+ ...def,
509
536
  noRuntimeInput: false
510
537
  });
511
538
  }
@@ -536,6 +563,7 @@ var SENSITIVE_BODY_KEYS = new Set([
536
563
  "refresh_token",
537
564
  "secret",
538
565
  "client_secret",
566
+ "clientsecret",
539
567
  "apikey",
540
568
  "api_key"
541
569
  ]);
@@ -626,16 +654,14 @@ async function validateResponse(schema, data, ctx) {
626
654
  }
627
655
  }
628
656
  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
657
  const timeoutController = new AbortController;
632
658
  const timeout = config.timeoutMs === undefined ? undefined : setTimeout(() => timeoutController.abort(), config.timeoutMs);
633
659
  const signal = mergeSignals(timeoutController.signal, config.userSignal);
634
660
  try {
635
- return await config.fetchFn(fullUrl, {
661
+ return await config.fetchFn(config.fullUrl, {
636
662
  method: config.method,
637
663
  headers: config.headers,
638
- body: buildFetchBody(config.body, config.headers),
664
+ body: buildFetchBody(config.body, config.headers, (msg) => new ConfigError(msg, ctx())),
639
665
  signal
640
666
  });
641
667
  } catch (error) {
@@ -648,6 +674,8 @@ async function httpRequest(config, ctx) {
648
674
  }
649
675
  throw new RequestAbortedError(ctx(), error);
650
676
  }
677
+ if (error instanceof ConfigError)
678
+ throw error;
651
679
  throw new NetworkError(error instanceof Error ? error.message : String(error), ctx(), error);
652
680
  } finally {
653
681
  if (timeout)
@@ -684,35 +712,26 @@ async function readErrorResponse(response) {
684
712
  };
685
713
  }
686
714
  function getRetryDelay(params) {
715
+ const maxDelay = params.maxRetryDelayMs ?? 30000;
687
716
  const { retryDelay, attempt, response } = params;
688
717
  if (response && (response.status === 429 || response.status === 503)) {
689
718
  const retryAfter = response.headers.get("retry-after");
690
719
  if (retryAfter) {
691
720
  const seconds = Number(retryAfter);
692
- if (Number.isFinite(seconds))
693
- return seconds * 1000;
721
+ if (Number.isFinite(seconds)) {
722
+ return Math.min(Math.max(0, seconds * 1000), maxDelay);
723
+ }
694
724
  const dateMs = Date.parse(retryAfter);
695
725
  if (!Number.isNaN(dateMs)) {
696
- return Math.max(0, dateMs - Date.now());
726
+ return Math.min(Math.max(0, dateMs - Date.now()), maxDelay);
697
727
  }
698
728
  }
699
729
  }
700
- if (typeof retryDelay === "function")
701
- return retryDelay(attempt);
702
- return retryDelay;
730
+ const delayMs = typeof retryDelay === "function" ? retryDelay(attempt) : retryDelay;
731
+ return Math.min(delayMs, maxDelay);
703
732
  }
704
733
  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;
734
+ return !isNonRetryableApiError(error);
716
735
  }
717
736
  function shouldAttemptRetryOnHttp(retryCtx) {
718
737
  const { error, response } = retryCtx;
@@ -725,6 +744,7 @@ async function makeRequestWithRetry(args) {
725
744
  config,
726
745
  maxRetries,
727
746
  retryDelay,
747
+ maxRetryDelayMs,
728
748
  validateFn,
729
749
  shouldRetry,
730
750
  logger,
@@ -763,7 +783,8 @@ async function makeRequestWithRetry(args) {
763
783
  const delayMs = getRetryDelay({
764
784
  retryDelay,
765
785
  attempt: retryCount,
766
- response
786
+ response,
787
+ maxRetryDelayMs
767
788
  });
768
789
  await onRetry?.({
769
790
  ...retryCtx,
@@ -778,12 +799,12 @@ async function makeRequestWithRetry(args) {
778
799
  throw new HttpResponseError(response.status, body, responseHeaders, ctx({ attempt: retryCount }));
779
800
  }
780
801
  return {
781
- validatedData: await validateFn(response),
802
+ validatedData: await validateFn(response, retryCount),
782
803
  retryCount,
783
804
  httpStatus: response.status
784
805
  };
785
806
  } catch (error) {
786
- if (error instanceof HttpResponseError || error instanceof ResponseValidationError || error instanceof RequestValidationError || error instanceof ConfigError || error instanceof RequestAbortedError) {
807
+ if (isNonRetryableApiError(error)) {
787
808
  throw error;
788
809
  }
789
810
  const retryCtx = {
@@ -800,7 +821,8 @@ async function makeRequestWithRetry(args) {
800
821
  const delayMs = getRetryDelay({
801
822
  retryDelay,
802
823
  attempt: retryCount,
803
- response: undefined
824
+ response: undefined,
825
+ maxRetryDelayMs
804
826
  });
805
827
  await onRetry?.({
806
828
  ...retryCtx,
@@ -814,19 +836,21 @@ async function makeRequestWithRetry(args) {
814
836
  }
815
837
  async function validateAndTransformResponse(endpoint, response, ctx) {
816
838
  const responseHeaders = getHeadersAsObject(response.headers);
839
+ const text = await response.text();
840
+ const responseSizeBytes = new TextEncoder().encode(text).length;
817
841
  let responseData;
818
842
  if (hasJsonContentType(responseHeaders)) {
819
- const text = await response.text();
820
843
  try {
821
844
  responseData = text === "" ? undefined : JSON.parse(text);
822
845
  } catch (error) {
823
846
  throw new ResponseValidationError(makeJsonParseZodError(error, text), text, ctx());
824
847
  }
825
848
  } else {
826
- responseData = await response.text();
849
+ responseData = text;
827
850
  }
828
851
  const transformedData = endpoint.resFormatter ? endpoint.resFormatter(responseData, responseHeaders) : responseData;
829
- return validateResponse(endpoint.resSchema, transformedData, ctx);
852
+ const data = await validateResponse(endpoint.resSchema, transformedData, ctx);
853
+ return { data, responseSizeBytes };
830
854
  }
831
855
  function defaultShouldRetry(ctx) {
832
856
  if (ctx.error)
@@ -845,6 +869,28 @@ function isActionInvokeOptionsOnly(value) {
845
869
  return false;
846
870
  return Object.keys(value).every((k) => k === "signal" || k === "meta");
847
871
  }
872
+ function resolveActionInvocation(action, actionName, first, second) {
873
+ if (!action.noRuntimeInput) {
874
+ return { input: first, options: second ?? {} };
875
+ }
876
+ if (first === undefined || first === null) {
877
+ return { input: undefined, options: second ?? {} };
878
+ }
879
+ if (second !== undefined) {
880
+ return { input: undefined, options: second };
881
+ }
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
+ }
848
894
  function createApiClientImpl(apiConfig) {
849
895
  let actions = {};
850
896
  const cache = createActionCache();
@@ -873,22 +919,12 @@ function createApiClientImpl(apiConfig) {
873
919
  throw await reportApiError(new ConfigError(`Unknown API endpoint: ${String(endpointKey)}`, ctx()), apiConfig);
874
920
  }
875
921
  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;
922
+ let pathParams;
888
923
  let queryParams;
889
924
  let body;
890
925
  let headers;
891
926
  try {
927
+ pathParams = buildPathParams(endpoint, rawInput.reqParams, ctx);
892
928
  queryParams = buildQueryParams(endpoint, rawInput.reqQuery, ctx);
893
929
  body = buildRequestBody(endpoint, rawInput.reqBody, ctx);
894
930
  headers = buildHeaders(endpoint, rawInput.reqHeaders, apiConfig.defaultHeaders ?? {}, ctx);
@@ -897,6 +933,25 @@ function createApiClientImpl(apiConfig) {
897
933
  throw await reportApiError(e, apiConfig);
898
934
  throw e;
899
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;
900
955
  baseContext.requestHeaders = headers;
901
956
  baseContext.requestBody = body;
902
957
  if (apiConfig.onRequest) {
@@ -921,11 +976,9 @@ function createApiClientImpl(apiConfig) {
921
976
  }
922
977
  const config = {
923
978
  method: endpoint.method,
924
- url: finalUrl,
925
- queryParams,
979
+ fullUrl,
926
980
  body,
927
981
  headers,
928
- doNotEncodeQueryParams: endpoint.doNotEncodeQueryParams,
929
982
  timeoutMs: input.timeoutMs ?? apiConfig.timeoutMs,
930
983
  fetchFn: input.fetch ?? apiConfig.fetch ?? fetch,
931
984
  userSignal: input.signal
@@ -935,7 +988,8 @@ function createApiClientImpl(apiConfig) {
935
988
  config,
936
989
  maxRetries,
937
990
  retryDelay: apiConfig.retryDelayMs ?? 300,
938
- validateFn: (response) => validateAndTransformResponse(endpoint, response, ctx),
991
+ maxRetryDelayMs: apiConfig.maxRetryDelayMs,
992
+ validateFn: (response, attempt) => validateAndTransformResponse(endpoint, response, (over) => ctx({ attempt, ...over })),
939
993
  shouldRetry: apiConfig.shouldRetry ?? defaultShouldRetry,
940
994
  logger: apiConfig.logger,
941
995
  ctx,
@@ -947,10 +1001,10 @@ function createApiClientImpl(apiConfig) {
947
1001
  onRetry: apiConfig.onRetry
948
1002
  });
949
1003
  const responseTimeMs = elapsed();
950
- const responseSizeMb = calculateSizeInMb(result.validatedData);
1004
+ const responseSizeMb = result.validatedData.responseSizeBytes / (1024 * 1024);
951
1005
  const requestBodySizeMb = calculateSizeInMb(config.body);
952
1006
  return {
953
- data: result.validatedData,
1007
+ data: result.validatedData.data,
954
1008
  requestBodySizeMb,
955
1009
  responseSizeMb,
956
1010
  responseTimeMs,
@@ -1006,22 +1060,14 @@ function createApiClientImpl(apiConfig) {
1006
1060
  throw await reportApiError(wrapped, apiConfig);
1007
1061
  }
1008
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
+ }
1009
1068
  const request = async (key, input, options) => {
1010
1069
  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);
1070
+ return dispatchAction(key, input, options);
1025
1071
  }
1026
1072
  return invokeEndpoint(key, input ?? {});
1027
1073
  };
@@ -1035,22 +1081,7 @@ function createApiClientImpl(apiConfig) {
1035
1081
  return (input) => invokeEndpoint(prop, input ?? {});
1036
1082
  }
1037
1083
  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);
1084
+ return (first, second) => dispatchAction(prop, first, second);
1054
1085
  }
1055
1086
  return;
1056
1087
  }
@@ -1078,72 +1109,306 @@ function createApiClientImpl(apiConfig) {
1078
1109
  return proxy;
1079
1110
  }
1080
1111
  var createApiClient = (apiConfig) => createApiClientImpl(apiConfig);
1081
- // src/secrets/infisical.ts
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
+ }
1124
+ }
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.");
1131
+ }
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);
1154
+ }
1155
+ }
1156
+
1157
+ // src/secrets/environment.ts
1158
+ function getProcessEnvironment() {
1159
+ return typeof process === "undefined" ? undefined : process.env;
1160
+ }
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.");
1166
+ }
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";
1189
+ }
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.");
1195
+ });
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
+ }
1248
+ }
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
+ }
1265
+ }
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
+ }
1270
+ }
1271
+
1272
+ // src/secrets/infisical-session.ts
1082
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 };
1295
+ }
1296
+
1297
+ // src/secrets/infisical.ts
1083
1298
  var DEFAULT_INFISICAL_SITE_URL = "https://eu.infisical.com";
1084
1299
  var defaultFetch = globalThis.fetch;
1085
- var infisicalAuthResponseSchema = z.object({
1086
- accessToken: z.string()
1300
+ var infisicalAuthResponseSchema = z2.object({ accessToken: z2.string() });
1301
+ var infisicalSecretSchema = z2.object({
1302
+ secretKey: z2.string(),
1303
+ secretValue: z2.string()
1087
1304
  });
1088
- var infisicalSecretSchema = z.object({
1089
- secretKey: z.string(),
1090
- secretValue: z.string()
1305
+ var infisicalSecretsResponseSchema = z2.object({
1306
+ secrets: z2.array(infisicalSecretSchema)
1091
1307
  });
1092
- var infisicalSecretsResponseSchema = z.object({
1093
- secrets: z.array(infisicalSecretSchema)
1094
- });
1095
- async function fetchInfisicalSecrets({
1096
- keys,
1097
- projectId,
1098
- environment,
1099
- siteUrl,
1100
- clientId,
1101
- clientSecret
1102
- }) {
1103
- const infisical = createInfisicalApiClient(siteUrl);
1104
- const { data: auth } = await infisical.request("login", {
1105
- reqBody: {
1106
- clientId,
1107
- 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);
1108
1329
  }
1109
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) {
1110
1387
  const { data } = await infisical.request("listSecrets", {
1111
- reqHeaders: {
1112
- Authorization: `Bearer ${auth.accessToken}`
1113
- },
1388
+ signal: options.signal,
1389
+ reqHeaders: { Authorization: `Bearer ${token}` },
1114
1390
  reqQuery: {
1115
- environment,
1116
- projectId,
1117
- secretPath: "/",
1391
+ environment: options.environment,
1392
+ projectId: options.projectId,
1393
+ secretPath: options.secretPath ?? "/",
1118
1394
  viewSecretValue: true,
1119
1395
  expandSecretReferences: true,
1120
1396
  includeImports: true
1121
1397
  }
1122
1398
  });
1123
- const wantedKeys = new Set(keys);
1399
+ const wantedKeys = new Set(options.keys);
1124
1400
  return Object.fromEntries(data.secrets.filter((secret) => wantedKeys.has(secret.secretKey)).map((secret) => [secret.secretKey, secret.secretValue]));
1125
1401
  }
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
- });
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 });
1147
1412
  }
1148
1413
  function createInfisicalApiClient(siteUrl) {
1149
1414
  return createApiClient({
@@ -1155,12 +1420,26 @@ function createInfisicalApiClient(siteUrl) {
1155
1420
  fetch: getFetch(),
1156
1421
  retries: 1,
1157
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
+ },
1158
1437
  login: {
1159
1438
  method: "POST",
1160
1439
  path: "/api/v1/auth/universal-auth/login",
1161
- reqBodySchema: z.object({
1162
- clientId: z.string(),
1163
- clientSecret: z.string()
1440
+ reqBodySchema: z2.object({
1441
+ clientId: z2.string(),
1442
+ clientSecret: z2.string()
1164
1443
  }),
1165
1444
  resSchema: infisicalAuthResponseSchema,
1166
1445
  reqBodyFormat: "json"
@@ -1168,16 +1447,16 @@ function createInfisicalApiClient(siteUrl) {
1168
1447
  listSecrets: {
1169
1448
  method: "GET",
1170
1449
  path: "/api/v4/secrets",
1171
- reqHeadersSchema: z.object({
1172
- Authorization: z.string()
1450
+ reqHeadersSchema: z2.object({
1451
+ Authorization: z2.string()
1173
1452
  }),
1174
- reqQuerySchema: z.object({
1175
- projectId: z.string(),
1176
- environment: z.string(),
1177
- secretPath: z.string(),
1178
- viewSecretValue: z.boolean(),
1179
- expandSecretReferences: z.boolean(),
1180
- 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()
1181
1460
  }),
1182
1461
  resSchema: infisicalSecretsResponseSchema
1183
1462
  }
@@ -1185,21 +1464,26 @@ function createInfisicalApiClient(siteUrl) {
1185
1464
  });
1186
1465
  }
1187
1466
  function getFetch() {
1188
- return globalThis.fetch ?? defaultFetch;
1467
+ const fetchFn = globalThis.fetch ?? defaultFetch;
1468
+ return (input, init) => {
1469
+ init?.signal?.throwIfAborted();
1470
+ return fetchFn(input, init);
1471
+ };
1189
1472
  }
1190
1473
 
1191
- // src/secrets/loaders.ts
1474
+ // src/secrets/worker-loader.ts
1192
1475
  function cloudflareWorkerEnvLoader(workerEnv) {
1193
- return async (ctx) => {
1194
- const out = {};
1476
+ const loader = async (ctx) => {
1477
+ const out = Object.create(null);
1195
1478
  for (const key of ctx.keys) {
1196
- const str = workerBindingToString(workerEnv[key]);
1479
+ const str = workerBindingToString(Object.hasOwn(workerEnv, key) ? workerEnv[key] : undefined);
1197
1480
  if (str !== undefined) {
1198
1481
  out[key] = str;
1199
1482
  }
1200
1483
  }
1201
1484
  return out;
1202
1485
  };
1486
+ return Object.assign(loader, { source: "Cloudflare Worker bindings" });
1203
1487
  }
1204
1488
  function workerBindingToString(value) {
1205
1489
  if (value === undefined || value === null) {
@@ -1216,21 +1500,30 @@ function workerBindingToString(value) {
1216
1500
  }
1217
1501
  return String(value);
1218
1502
  }
1503
+
1504
+ // src/secrets/loaders.ts
1219
1505
  function infisicalLoader(config) {
1220
- return async (ctx) => fetchInfisicalSecrets({
1506
+ validateInfisicalOrganizationId(config.organizationId);
1507
+ const loader = async (ctx) => fetchInfisicalSecrets({
1221
1508
  keys: ctx.keys,
1222
1509
  projectId: config.projectId,
1510
+ organizationId: config.organizationId,
1223
1511
  environment: ctx.environment,
1224
1512
  siteUrl: config.siteUrl ?? DEFAULT_INFISICAL_SITE_URL,
1225
1513
  clientId: config.clientId,
1226
- clientSecret: config.clientSecret
1514
+ clientSecret: config.clientSecret,
1515
+ secretPath: config.secretPath,
1516
+ timeoutMs: config.timeoutMs ?? ctx.timeoutMs,
1517
+ signal: ctx.signal
1227
1518
  });
1519
+ return Object.assign(loader, { source: "Infisical HTTP" });
1228
1520
  }
1229
1521
  async function processEnvLoader(_ctx) {
1230
1522
  return {};
1231
1523
  }
1524
+ Object.assign(processEnvLoader, { source: "process.env" });
1232
1525
  export {
1233
- processEnvLoader,
1526
+ cloudflareWorkerEnvLoader,
1234
1527
  infisicalLoader,
1235
- cloudflareWorkerEnvLoader
1528
+ processEnvLoader
1236
1529
  };