@ciq-dev/neoiq-foundation-node 1.0.2 → 1.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, VaultCacheConfigSchema, VaultConfigSchema, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-BwTyDuEv.mjs";
1
+ import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, VaultConfigSchema, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-DUFkYSRe.mjs";
2
2
  import { resourceFromAttributes } from "@opentelemetry/resources";
3
3
  import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
4
4
  import { SpanStatusCode as SpanStatusCode$1, context as context$1, metrics, propagation as propagation$1, trace as trace$1 } from "@opentelemetry/api";
@@ -733,109 +733,100 @@ function createHttpClient(options) {
733
733
 
734
734
  //#endregion
735
735
  //#region src/features/vault.ts
736
+ const DEFAULTS = {
737
+ address: "https://vault.beta.commerceiq.ai",
738
+ authMethod: "kubernetes",
739
+ mountPoint: "secret",
740
+ tokenPath: "/var/run/secrets/kubernetes.io/serviceaccount/token",
741
+ k8sAuthMount: "kubernetes"
742
+ };
743
+ function stripSlashes(value) {
744
+ return value.replace(/^\/+|\/+$/g, "");
745
+ }
736
746
  async function loadNodeVault() {
737
747
  try {
738
748
  const mod = await import("node-vault");
739
749
  return mod.default ?? mod;
740
750
  } catch {
741
- throw new Error("node-vault is not installed. Install the optional peer dependency: npm install node-vault");
751
+ throw new Error("node-vault is not installed. Install it as a peer dependency: npm install node-vault");
742
752
  }
743
753
  }
744
754
  var VaultClient = class {
745
- client = null;
755
+ logger;
756
+ address;
757
+ authMethod;
758
+ role;
759
+ mountPoint;
760
+ tokenPath;
761
+ k8sAuthMount;
762
+ namespace;
746
763
  clientPromise;
747
764
  authenticated = false;
748
- cache = new Map();
749
- config;
750
- logger;
751
- cacheEnabled;
752
- cacheTtlMs;
765
+ /** In-flight auth so concurrent getSecret() calls don't all trigger kubernetesLogin. */
766
+ authPromise = null;
753
767
  constructor(options) {
754
- this.config = {
755
- address: "https://vault.beta.commerceiq.ai/",
756
- authMethod: "kubernetes",
757
- role: "",
758
- mountPoint: "secret",
759
- tokenPath: "/var/run/secrets/kubernetes.io/serviceaccount/token",
760
- ...options.config
761
- };
762
768
  this.logger = options.logger;
763
- this.cacheEnabled = this.config.cache?.enabled !== false;
764
- this.cacheTtlMs = (this.config.cache?.ttlSeconds ?? 300) * 1e3;
765
- this.clientPromise = this.initClient();
766
- }
767
- async initClient() {
768
- const nodeVault = await loadNodeVault();
769
- const client = nodeVault({
769
+ const cfg = options.config;
770
+ this.address = (cfg.address ?? DEFAULTS.address).replace(/\/+$/, "");
771
+ this.authMethod = cfg.authMethod ?? DEFAULTS.authMethod;
772
+ this.role = cfg.role;
773
+ this.mountPoint = stripSlashes(cfg.mountPoint ?? DEFAULTS.mountPoint);
774
+ this.tokenPath = cfg.tokenPath ?? DEFAULTS.tokenPath;
775
+ this.k8sAuthMount = cfg.k8sAuthMount ?? DEFAULTS.k8sAuthMount;
776
+ this.namespace = cfg.namespace;
777
+ const factory = options.nodeVaultFactory ? Promise.resolve(options.nodeVaultFactory) : loadNodeVault();
778
+ this.clientPromise = factory.then((create) => create({
770
779
  apiVersion: "v1",
771
- endpoint: this.config.address,
772
- namespace: this.config.namespace
773
- });
774
- this.client = client;
775
- return client;
780
+ endpoint: this.address,
781
+ namespace: this.namespace
782
+ }));
776
783
  }
777
784
  async authenticate() {
778
785
  if (this.authenticated) return;
786
+ if (this.authPromise) return this.authPromise;
787
+ this.authPromise = this.doAuthenticate().finally(() => {
788
+ if (!this.authenticated) this.authPromise = null;
789
+ });
790
+ return this.authPromise;
791
+ }
792
+ async doAuthenticate() {
779
793
  const client = await this.clientPromise;
780
- if (this.config.authMethod === "token") {
794
+ if (this.authMethod === "token") {
781
795
  const token = process.env.VAULT_TOKEN;
782
- if (!token) throw new Error("VAULT_TOKEN environment variable is required for token-based authentication");
796
+ if (!token) throw new Error("VAULT_TOKEN env var is required for token auth");
783
797
  client.token = token;
784
798
  this.authenticated = true;
785
799
  this.logger.info({ authMethod: "token" }, "Vault authenticated via token");
786
800
  return;
787
801
  }
788
- const role = this.config.role;
789
- if (!role) throw new Error("Vault role is required for Kubernetes authentication. Set vault.role in config or VAULT_AUTH_ROLE env var.");
802
+ if (!this.role) throw new Error("Vault role is required for Kubernetes auth (set vault.role or VAULT_AUTH_ROLE)");
790
803
  let jwt;
791
804
  try {
792
- jwt = await readFile(this.config.tokenPath, "utf8");
805
+ jwt = await readFile(this.tokenPath, "utf8");
793
806
  } catch (err) {
794
- const message = err instanceof Error ? err.message : String(err);
795
- throw new Error(`Failed to read Kubernetes service account token from ${this.config.tokenPath}: ${message}`);
807
+ const msg = err instanceof Error ? err.message : String(err);
808
+ throw new Error(`Failed to read service-account token at ${this.tokenPath}: ${msg}`);
796
809
  }
797
810
  const result = await client.kubernetesLogin({
798
- role,
799
- jwt
811
+ role: this.role,
812
+ jwt,
813
+ mount_point: this.k8sAuthMount
800
814
  });
801
815
  client.token = result.auth.client_token;
802
816
  this.authenticated = true;
803
817
  this.logger.info({
804
818
  authMethod: "kubernetes",
805
- role,
819
+ role: this.role,
806
820
  leaseDuration: result.auth.lease_duration
807
821
  }, "Vault authenticated via Kubernetes service account");
808
822
  }
809
823
  async getSecret(path) {
810
- const cacheKey = path;
811
- if (this.cacheEnabled) {
812
- const cached = this.cache.get(cacheKey);
813
- if (cached && cached.expiresAt > Date.now()) {
814
- this.logger.debug({
815
- path,
816
- cache: "hit"
817
- }, "Vault secret cache hit");
818
- return cached.data;
819
- }
820
- }
821
824
  await this.authenticate();
822
825
  const client = await this.clientPromise;
823
- const fullPath = `${this.config.mountPoint}/data/${path}`;
826
+ const fullPath = `${this.mountPoint}/data/${stripSlashes(path)}`;
824
827
  this.logger.debug({ path: fullPath }, "Reading secret from Vault");
825
828
  const response = await client.read(fullPath);
826
- const data = response.data.data;
827
- if (this.cacheEnabled) {
828
- this.cache.set(cacheKey, {
829
- data,
830
- expiresAt: Date.now() + this.cacheTtlMs
831
- });
832
- this.logger.debug({
833
- path,
834
- cache: "miss",
835
- ttlMs: this.cacheTtlMs
836
- }, "Vault secret cached");
837
- }
838
- return data;
829
+ return response.data.data;
839
830
  }
840
831
  async getSecretValue(path, key) {
841
832
  const secrets = await this.getSecret(path);
@@ -844,10 +835,6 @@ var VaultClient = class {
844
835
  isAuthenticated() {
845
836
  return this.authenticated;
846
837
  }
847
- clearCache() {
848
- this.cache.clear();
849
- this.logger.debug({}, "Vault secret cache cleared");
850
- }
851
838
  async checkHealth() {
852
839
  try {
853
840
  await this.authenticate();
@@ -878,12 +865,11 @@ function warnDeprecation(oldPath, newPath, logger) {
878
865
  function createFoundation(input) {
879
866
  const startTime = Date.now();
880
867
  const config = parseConfig(input);
881
- const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig, vault: vaultConfig } = config;
868
+ const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig } = config;
882
869
  const features = {
883
870
  tracing: featuresConfig.tracing ?? true,
884
871
  metrics: featuresConfig.metrics ?? true,
885
- logging: featuresConfig.logging ?? true,
886
- vault: vaultConfig.enabled ?? false
872
+ logging: featuresConfig.logging ?? true
887
873
  };
888
874
  const contextManager = createContextManager();
889
875
  let logger;
@@ -961,22 +947,6 @@ function createFoundation(input) {
961
947
  metricsError = err.message;
962
948
  logger.error({ error: metricsError }, "Metrics setup failed - continuing without metrics");
963
949
  }
964
- let vaultClient = null;
965
- let vaultError;
966
- if (features.vault) try {
967
- vaultClient = createVaultClient({
968
- config: vaultConfig,
969
- logger
970
- });
971
- logger.info({
972
- feature: "vault",
973
- address: vaultConfig.address,
974
- authMethod: vaultConfig.authMethod
975
- }, "Vault client created (lazy auth on first secret read)");
976
- } catch (err) {
977
- vaultError = err.message;
978
- logger.error({ error: vaultError }, "Vault setup failed - continuing without vault");
979
- }
980
950
  logger.info({
981
951
  serviceName,
982
952
  serviceVersion,
@@ -1044,45 +1014,50 @@ function createFoundation(input) {
1044
1014
  ...options,
1045
1015
  foundation
1046
1016
  }) };
1047
- const secretsModule = vaultClient;
1017
+ let secrets = null;
1018
+ if (config.vault?.enabled) try {
1019
+ secrets = createVaultClient({
1020
+ config: config.vault,
1021
+ logger
1022
+ });
1023
+ logger.info({
1024
+ address: config.vault.address,
1025
+ authMethod: config.vault.authMethod
1026
+ }, "Vault secrets module initialized");
1027
+ } catch (err) {
1028
+ logger.error({ error: err.message }, "Vault initialization failed - continuing without secrets");
1029
+ }
1048
1030
  const buildHealthStatus = () => {
1049
1031
  const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
1050
1032
  const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
1051
1033
  const loggingUp = !loggingError;
1052
- const vaultUp = !features.vault || !vaultError && !!vaultClient;
1053
- const allUp = tracingUp && metricsUp && loggingUp && vaultUp;
1054
- const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && (!vaultUp || !features.vault) && !loggingUp;
1034
+ const allUp = tracingUp && metricsUp && loggingUp;
1035
+ const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
1055
1036
  let status = "healthy";
1056
1037
  if (!allUp) status = allDown ? "unhealthy" : "degraded";
1057
- const components = {
1058
- tracing: {
1059
- enabled: features.tracing,
1060
- status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
1061
- message: tracingError
1062
- },
1063
- metrics: {
1064
- enabled: features.metrics,
1065
- status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
1066
- message: metricsError
1067
- },
1068
- logging: {
1069
- enabled: features.logging,
1070
- status: loggingError ? "down" : "up",
1071
- message: loggingError
1072
- }
1073
- };
1074
- if (features.vault) components.vault = {
1075
- enabled: true,
1076
- status: vaultError ? "down" : "up",
1077
- message: vaultError
1078
- };
1079
1038
  return {
1080
1039
  status,
1081
1040
  timestamp: new Date().toISOString(),
1082
1041
  service: serviceName,
1083
1042
  version: serviceVersion,
1084
1043
  uptime: Math.floor((Date.now() - startTime) / 1e3),
1085
- components
1044
+ components: {
1045
+ tracing: {
1046
+ enabled: features.tracing,
1047
+ status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
1048
+ message: tracingError
1049
+ },
1050
+ metrics: {
1051
+ enabled: features.metrics,
1052
+ status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
1053
+ message: metricsError
1054
+ },
1055
+ logging: {
1056
+ enabled: features.logging,
1057
+ status: loggingError ? "down" : "up",
1058
+ message: loggingError
1059
+ }
1060
+ }
1086
1061
  };
1087
1062
  };
1088
1063
  const shutdownFn = async () => {
@@ -1090,7 +1065,6 @@ function createFoundation(input) {
1090
1065
  const promises = [];
1091
1066
  if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
1092
1067
  if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
1093
- if (vaultClient) vaultClient.clearCache();
1094
1068
  await Promise.all(promises);
1095
1069
  logger.info({}, "Foundation shutdown complete");
1096
1070
  };
@@ -1123,7 +1097,7 @@ function createFoundation(input) {
1123
1097
  observability,
1124
1098
  http: httpModule,
1125
1099
  lifecycle,
1126
- secrets: secretsModule,
1100
+ secrets,
1127
1101
  logger,
1128
1102
  context: contextManager,
1129
1103
  fastifyPlugin: createObservabilityPlugin({
@@ -1469,5 +1443,5 @@ var InMemoryObjectStore = class {
1469
1443
  };
1470
1444
 
1471
1445
  //#endregion
1472
- export { AutoInstrumentationConfigSchema, AwsS3ObjectStore, FeaturesConfigSchema, FoundationConfigSchema, InMemoryObjectStore, LoggingConfigSchema, OtelConfigSchema, REDACT_PATHS, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, VaultCacheConfigSchema, VaultClient, VaultConfigSchema, buildPinoRedactConfig, context, createContextManager, createFallbackLogger, createFoundation, createHttpClient, createLogger, createObservabilityPlugin, createVaultClient, getActiveSpan, getDefaultOtelEndpoint, getGlobalLogger, getMeter, getTraceContext, getTracer, isMetricsEnabled, isTracingEnabled, metrics, parseConfig, propagation, sanitizeBody, setGlobalLogger, setupMetrics, setupObservability, setupTracing, shutdownMetrics, shutdownTracing, trace };
1446
+ export { AutoInstrumentationConfigSchema, AwsS3ObjectStore, FeaturesConfigSchema, FoundationConfigSchema, InMemoryObjectStore, LoggingConfigSchema, OtelConfigSchema, REDACT_PATHS, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, VaultClient, VaultConfigSchema, buildPinoRedactConfig, context, createContextManager, createFallbackLogger, createFoundation, createHttpClient, createLogger, createObservabilityPlugin, createVaultClient, getActiveSpan, getDefaultOtelEndpoint, getGlobalLogger, getMeter, getTraceContext, getTracer, isMetricsEnabled, isTracingEnabled, metrics, parseConfig, propagation, sanitizeBody, setGlobalLogger, setupMetrics, setupObservability, setupTracing, shutdownMetrics, shutdownTracing, trace };
1473
1447
  //# sourceMappingURL=index.mjs.map